Skip to main content

service_bindings/
binding.rs

1/*
2 * Copyright 2021 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::cell::RefCell;
18use std::collections::hash_map::Entry;
19use std::collections::HashMap;
20use std::fmt::Debug;
21use std::fs;
22use std::path::PathBuf;
23use std::str;
24
25use crate::secret;
26
27/// The key for the provider of a `Binding`.
28pub const PROVIDER: &str = "provider";
29
30/// The key for the type of a `Binding`.
31pub const TYPE: &str = "type";
32
33/// An error returned when an invalid `Binding` is encountered.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct InvalidBindingError {
36    message: String,
37}
38
39impl InvalidBindingError {
40    pub fn new(message: impl Into<String>) -> InvalidBindingError {
41        InvalidBindingError { message: message.into() }
42    }
43}
44
45/// A representation of a binding as defined by the
46/// [Kubernetes Service Binding Specification](https://github.com/k8s-service-bindings/spec#workload-projection).
47pub trait Binding {
48    /// Returns the contents of a `Binding` entry in its raw bytes form.
49    ///
50    /// * `key` - the key of the entry to retrieve
51    ///
52    /// returns the contents of a `Binding` entry if it exists, otherwise `None`
53    fn get_as_bytes(&self, key: &str) -> Option<Vec<u8>>;
54
55    /// Returns the name of the `Binding`
56    ///
57    /// returns the name of the `Binding`
58    fn get_name(&self) -> String;
59
60    /// Returns the contents of a `Binding` entry as a UTF-8 decoded `str`.  Any whitespace is trimmed.
61    ///
62    /// * `key` - the key of the entry to retrieve
63    ///
64    /// returns the contents of a `Binding` entry as a UTF-8 decoded `str` if it exists, otherwise `None`
65    fn get(&self, key: &str) -> Option<String> {
66        return match self.get_as_bytes(key) {
67            None => None,
68            Some(b) => Some(str::from_utf8(&b)
69                .map(|s| s.trim().to_string())
70                .unwrap()),
71        };
72    }
73
74    /// Returns the value of the `PROVIDER` key.
75    ///
76    /// returns the value of the `PROVIDER` key if it exists, otherwise `None`
77    fn get_provider(&self) -> Option<String> {
78        return self.get(PROVIDER);
79    }
80
81    /// Returns the value of the `TYPE` key.
82    ///
83    /// returns the value of the `TYPE` key
84    fn get_type(&self) -> Result<String, InvalidBindingError> {
85        return match self.get(TYPE) {
86            None => Err(InvalidBindingError::new("binding does not contain a type")),
87            Some(t) => Ok(t),
88        };
89    }
90}
91
92/// An implementation of `Binding` that caches values once they've been retrieved.
93pub struct CacheBinding<'a> {
94    delegate: Box<dyn Binding + 'a>,
95    cache: RefCell<HashMap<String, Vec<u8>>>,
96}
97
98impl<'a> CacheBinding<'a> {
99    /// Creates a new instance.
100    ///
101    /// * `delegate` - the `Binding` used to retrieve the original values
102    pub fn new(delegate: impl Binding + 'a) -> CacheBinding<'a> {
103        return CacheBinding {
104            delegate: Box::new(delegate),
105            cache: RefCell::new(HashMap::new()),
106        };
107    }
108}
109
110impl Binding for CacheBinding<'_> {
111    fn get_as_bytes(&self, key: &str) -> Option<Vec<u8>> {
112        return match self.cache.borrow_mut().entry(key.to_string()) {
113            Entry::Occupied(o) => Some(o.get().to_vec()),
114            Entry::Vacant(v) => {
115                return match self.delegate.get_as_bytes(key) {
116                    None => None,
117                    Some(w) => Some(v.insert(w).to_vec()),
118                };
119            }
120        };
121    }
122
123    fn get_name(&self) -> String {
124        return self.delegate.get_name();
125    }
126}
127
128/// An implementation of `Binding` that reads files from a volume mounted
129/// [Kubernetes Secret](https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets).
130pub struct ConfigTreeBinding {
131    root: PathBuf,
132}
133
134impl ConfigTreeBinding {
135    /// Creates a new instance.
136    ///
137    /// * `root` - the root of the volume mounted Kubernetes Secret
138    pub fn new<P: Into<PathBuf>>(root: P) -> ConfigTreeBinding {
139        return ConfigTreeBinding {
140            root: root.into()
141        };
142    }
143}
144
145impl Binding for ConfigTreeBinding {
146    fn get_as_bytes(&self, key: &str) -> Option<Vec<u8>> {
147        if !secret::is_valid_secret_key(key) {
148            return None;
149        }
150
151        let p = self.root.join(PathBuf::from(key));
152
153        if !p.exists() || !p.is_file() {
154            return None;
155        }
156
157        return fs::read(p).ok();
158    }
159
160    fn get_name(&self) -> String {
161        return self.root.file_stem()
162            .and_then(|s| s.to_str())
163            .map(|s| s.to_string())
164            .unwrap();
165    }
166}
167
168/// An implementation of `Binding` that returns values from a `HashMap`.
169pub struct HashMapBinding {
170    name: String,
171    content: HashMap<String, Vec<u8>>,
172}
173
174impl HashMapBinding {
175    /// Creates a new instance.
176    ///
177    /// * `name` - the name of the `Binding`
178    /// * `content` - the content of the `Binding`
179    pub fn new(name: impl Into<String>, content: HashMap<String, Vec<u8>>) -> HashMapBinding {
180        return HashMapBinding {
181            name: name.into(),
182            content,
183        };
184    }
185}
186
187impl Binding for HashMapBinding {
188    fn get_as_bytes(&self, key: &str) -> Option<Vec<u8>> {
189        if !secret::is_valid_secret_key(key) {
190            return None;
191        }
192
193        if !self.content.contains_key(key) {
194            return None;
195        }
196
197        return self.content.get(key)
198            .map(|v| v.to_vec());
199    }
200
201    fn get_name(&self) -> String {
202        return self.name.to_string();
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use std::cell::RefCell;
209    use std::collections::HashMap;
210    use std::rc::Rc;
211
212    use crate::binding::{Binding, CacheBinding, ConfigTreeBinding, HashMapBinding, InvalidBindingError};
213
214    #[test]
215    fn get_missing() {
216        let b = HashMapBinding::new("test-name", map! {});
217        assert_eq!(None, b.get("test-missing-key"))
218    }
219
220    #[test]
221    fn get_valid() {
222        let b = HashMapBinding::new("test-name", map! {
223            "test-secret-key" => "test-secret-value\n",
224        });
225
226        assert_eq!(Some("test-secret-value".to_string()), b.get("test-secret-key"))
227    }
228
229    #[test]
230    fn get_provider_missing() {
231        let b = HashMapBinding::new("test-name", HashMap::new());
232        assert_eq!(None, b.get_provider())
233    }
234
235    #[test]
236    fn get_provider_valid() {
237        let b = HashMapBinding::new("test-name", map! {
238            "provider" => "test-provider-1",
239        });
240
241        assert_eq!(Some("test-provider-1".to_string()), b.get_provider())
242    }
243
244    #[test]
245    fn get_type_invalid() {
246        let b = HashMapBinding::new("test-name", HashMap::new());
247        assert_eq!(Err(InvalidBindingError::new("binding does not contain a type")), b.get_type())
248    }
249
250    #[test]
251    fn get_type_valid() {
252        let b = HashMapBinding::new("test-name", map! {
253            "type" => "test-type-1",
254        });
255
256        assert_eq!(Ok("test-type-1".to_string()), b.get_type())
257    }
258
259    #[test]
260    fn cache_binding_missing() {
261        let s = StubBinding::new();
262        let c = Rc::clone(&s.get_as_bytes_count);
263
264        let b = CacheBinding::new(s);
265
266        assert_eq!(None, b.get_as_bytes("test-unknown-key"));
267        assert_eq!(None, b.get_as_bytes("test-unknown-key"));
268        assert_eq!(2, c.take());
269    }
270
271    #[test]
272    fn cache_binding_valid() {
273        let s = StubBinding::new();
274        let c = Rc::clone(&s.get_as_bytes_count);
275
276        let b = CacheBinding::new(s);
277
278        assert_eq!(Some(Vec::new()), b.get_as_bytes("test-secret-key"));
279        assert_eq!(Some(Vec::new()), b.get_as_bytes("test-secret-key"));
280        assert_eq!(1, c.take());
281    }
282
283    #[test]
284    fn cache_binding_get_name() {
285        let s = StubBinding::new();
286        let c = Rc::clone(&s.get_name_count);
287
288        let b = CacheBinding::new(s);
289
290        assert_eq!(String::from("test-name"), b.get_name());
291        assert_eq!(String::from("test-name"), b.get_name());
292        assert_eq!(2, c.take());
293    }
294
295    #[test]
296    fn config_tree_binding_missing() {
297        let b = ConfigTreeBinding::new("testdata/test-k8s");
298        assert_eq!(None, b.get_as_bytes("test-missing-key"))
299    }
300
301    #[test]
302    fn config_tree_binding_directory() {
303        let b = ConfigTreeBinding::new("testdata/test-k8s");
304        assert_eq!(None, b.get_as_bytes(".hidden-data"))
305    }
306
307    #[test]
308    fn config_tree_binding_invalid() {
309        let b = ConfigTreeBinding::new("testdata/test-k8s");
310        assert_eq!(None, b.get_as_bytes("test^invalid^key"))
311    }
312
313    #[test]
314    fn config_tree_binding_valid() {
315        let b = ConfigTreeBinding::new("testdata/test-k8s");
316        assert_eq!(Some("test-secret-value\n".as_bytes().to_vec()), b.get_as_bytes("test-secret-key"))
317    }
318
319    #[test]
320    fn config_tree_binding_get_name() {
321        let b = ConfigTreeBinding::new("testdata/test-k8s");
322        assert_eq!(String::from("test-k8s"), b.get_name())
323    }
324
325    #[test]
326    fn hash_map_binding_missing() {
327        let b = HashMapBinding::new("test-name", HashMap::new());
328        assert_eq!(None, b.get_as_bytes("test-missing-key"))
329    }
330
331    #[test]
332    fn hash_map_binding_invalid() {
333        let b = HashMapBinding::new("test-name", HashMap::new());
334        assert_eq!(None, b.get_as_bytes("test^invalid^key"))
335    }
336
337    #[test]
338    fn hash_map_binding_valid() {
339        let b = HashMapBinding::new("test-name", map! {
340            "test-secret-key" => "test-secret-value\n",
341        });
342
343        assert_eq!(Some("test-secret-value\n".as_bytes().to_vec()), b.get_as_bytes("test-secret-key"))
344    }
345
346    #[test]
347    fn hash_map_binding_get_name() {
348        let b = HashMapBinding::new("test-name", HashMap::new());
349        assert_eq!("test-name", b.get_name())
350    }
351
352    struct StubBinding {
353        get_as_bytes_count: Rc<RefCell<i32>>,
354        get_name_count: Rc<RefCell<i32>>,
355    }
356
357    impl StubBinding {
358        fn new() -> StubBinding {
359            return StubBinding {
360                get_as_bytes_count: Rc::new(RefCell::new(0)),
361                get_name_count: Rc::new(RefCell::new(0)),
362            };
363        }
364    }
365
366    impl Binding for StubBinding {
367        fn get_as_bytes(&self, key: &str) -> Option<Vec<u8>> {
368            (*self.get_as_bytes_count).replace_with(|f| *f + 1);
369
370            if "test-secret-key".eq(key) {
371                return Some(Vec::new());
372            }
373
374            return None;
375        }
376
377        fn get_name(&self) -> String {
378            (*self.get_name_count).replace_with(|f| *f + 1);
379            return String::from("test-name");
380        }
381    }
382}