1use std::sync::{LazyLock, OnceLock};
25
26pub const DEFAULT_IDENTITY_NAME: &str = "pingap";
28
29const MAX_LABELS: usize = 16;
31const MAX_LABEL_KEY: usize = 32;
32const MAX_LABEL_VALUE: usize = 128;
33const MAX_NAME: usize = 64;
34
35#[derive(Debug, Clone, PartialEq)]
36pub struct Identity {
37 pub name: String,
39 pub labels: Vec<(String, String)>,
42}
43
44impl Default for Identity {
45 fn default() -> Self {
46 Self {
47 name: DEFAULT_IDENTITY_NAME.to_string(),
48 labels: vec![],
49 }
50 }
51}
52
53impl Identity {
54 pub fn new(name: Option<&str>, labels: Vec<(String, String)>) -> Self {
57 let name = name
58 .map(str::trim)
59 .filter(|name| !name.is_empty())
60 .unwrap_or(DEFAULT_IDENTITY_NAME);
61 Self {
62 name: name.to_string(),
63 labels,
64 }
65 }
66 pub fn is_named(&self) -> bool {
68 self.name != DEFAULT_IDENTITY_NAME
69 }
70 pub fn label(&self, key: &str) -> Option<&str> {
72 self.labels
73 .iter()
74 .find(|(name, _)| name == key)
75 .map(|(_, value)| value.as_str())
76 }
77}
78
79pub fn parse_identity_name(name: Option<&str>) -> Result<(), String> {
83 let name = name.unwrap_or_default().trim();
84 if name.len() > MAX_NAME || name.chars().any(char::is_control) {
85 return Err(format!(
86 "name should be one line of at most {MAX_NAME} bytes"
87 ));
88 }
89 Ok(())
90}
91
92static IDENTITY: OnceLock<Identity> = OnceLock::new();
93static DEFAULT_IDENTITY: LazyLock<Identity> = LazyLock::new(Identity::default);
94
95pub fn set_identity(identity: Identity) {
98 IDENTITY.get_or_init(|| identity);
99}
100
101pub fn get_identity() -> &'static Identity {
104 IDENTITY.get().unwrap_or(&DEFAULT_IDENTITY)
105}
106
107pub fn parse_labels(
112 entries: &[String],
113) -> Result<Vec<(String, String)>, String> {
114 if entries.len() > MAX_LABELS {
115 return Err(format!(
116 "labels: {} of them, the most is {MAX_LABELS}",
117 entries.len()
118 ));
119 }
120 let mut labels: Vec<(String, String)> = Vec::with_capacity(entries.len());
121 for item in entries {
122 let invalid = |what: &str| {
123 let key = item.split(':').next().unwrap_or_default().trim();
124 format!("labels: {key:?} {what}")
125 };
126 let (key, value) = item
127 .split_once(':')
128 .ok_or_else(|| invalid("should be `key:value`"))?;
129 let (key, value) = (key.trim(), value.trim());
130 let named = !key.is_empty()
131 && key.len() <= MAX_LABEL_KEY
132 && key
133 .chars()
134 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
135 if !named {
136 return Err(invalid(
137 "should be a key of letters, digits, _ and -, at most 32",
138 ));
139 }
140 if value.is_empty() {
141 return Err(invalid("has no value"));
142 }
143 if value.starts_with('$') {
147 return Err(invalid(
148 "has a value that starts with $, which is read as nothing: the value of a label comes from the environment as $ENV:NAME",
149 ));
150 }
151 if value.len() > MAX_LABEL_VALUE || value.chars().any(char::is_control)
152 {
153 return Err(invalid(
154 "has a value that is longer than 128 bytes, or not one line of text",
155 ));
156 }
157 if labels.iter().any(|(name, _)| name == key) {
158 return Err(invalid("is there twice"));
159 }
160 labels.push((key.to_string(), value.to_string()));
161 }
162 Ok(labels)
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use pretty_assertions::assert_eq;
169
170 #[test]
171 fn test_identity() {
172 let identity = Identity::default();
173 assert_eq!("pingap", identity.name);
174 assert_eq!(false, identity.is_named());
175 assert_eq!(None, identity.label("cluster"));
176 assert_eq!("pingap", Identity::new(None, vec![]).name);
178 assert_eq!("pingap", Identity::new(Some(" "), vec![]).name);
179
180 let labels = parse_labels(&[
181 "cluster:sh-prod".to_string(),
182 " env : prod ".to_string(),
183 ])
184 .unwrap();
185 let identity = Identity::new(Some(" edge-sh "), labels);
186 assert_eq!("edge-sh", identity.name);
187 assert_eq!(true, identity.is_named());
188 assert_eq!(Some("sh-prod"), identity.label("cluster"));
189 assert_eq!(Some("prod"), identity.label("env"));
190 assert_eq!(None, identity.label("region"));
191
192 assert_eq!(Ok(()), parse_identity_name(None));
193 assert_eq!(Ok(()), parse_identity_name(Some("edge-sh 上海")));
194 assert_eq!(true, parse_identity_name(Some("a\nb")).is_err());
195 assert_eq!(true, parse_identity_name(Some(&"n".repeat(65))).is_err());
196 assert_eq!("pingap", get_identity().name);
198 }
199
200 #[test]
201 fn test_parse_labels() {
202 let parse = |entries: &[&str]| {
203 let entries: Vec<String> =
204 entries.iter().map(|item| item.to_string()).collect();
205 parse_labels(&entries)
206 };
207 assert_eq!(Ok(vec![]), parse(&[]));
208 assert_eq!(
210 Ok(vec![("zone".to_string(), "cn-east:a".to_string())]),
211 parse(&["zone:cn-east:a"])
212 );
213 for (entries, reason) in [
214 (vec!["cluster"], "\"cluster\" should be `key:value`"),
215 (vec![":sh"], "\"\" should be a key of"),
216 (vec!["k8s.cluster:sh"], "\"k8s.cluster\" should be a key of"),
217 (vec!["my cluster:sh"], "should be a key of"),
218 (vec!["cluster:"], "\"cluster\" has no value"),
219 (vec!["cluster: "], "\"cluster\" has no value"),
220 (vec!["cluster:a\nb"], "not one line of text"),
221 (vec!["cluster:$FILE:/etc/cluster"], "starts with $"),
222 (vec!["cluster:$CLUSTER"], "starts with $"),
223 (vec!["cluster:a", "cluster:b"], "\"cluster\" is there twice"),
224 ] {
225 let error = parse(&entries).unwrap_err();
226 assert_eq!(true, error.contains(reason), "{entries:?}: {error}");
227 }
228 let long = format!("cluster:{}", "a".repeat(129));
229 assert_eq!(true, parse(&[&long]).is_err());
230 let key = format!("{}:a", "k".repeat(33));
231 assert_eq!(true, parse(&[&key]).is_err());
232 let many: Vec<String> = (0..17).map(|i| format!("k{i}:v")).collect();
233 let many: Vec<&str> = many.iter().map(String::as_str).collect();
234 assert_eq!(true, parse(&many).unwrap_err().contains("the most is 16"));
235 }
236}