Skip to main content

zombienet_configuration/shared/
helpers.rs

1use std::{cell::RefCell, collections::HashSet, rc::Rc};
2
3use support::constants::{BORROWABLE, THIS_IS_A_BUG};
4use tracing::warn;
5
6use super::{
7    errors::ValidationError,
8    types::{ParaId, Port, ValidationContext},
9};
10
11pub fn merge_errors(errors: Vec<anyhow::Error>, new_error: anyhow::Error) -> Vec<anyhow::Error> {
12    let mut errors = errors;
13    errors.push(new_error);
14
15    errors
16}
17
18pub fn merge_errors_vecs(
19    errors: Vec<anyhow::Error>,
20    new_errors: Vec<anyhow::Error>,
21) -> Vec<anyhow::Error> {
22    let mut errors = errors;
23
24    for new_error in new_errors.into_iter() {
25        errors.push(new_error);
26    }
27
28    errors
29}
30
31/// Generates a unique name from a base name and the names already present in a
32/// [`ValidationContext`].
33///
34/// Uses [`generate_unique_node_name_from_names()`] internally to ensure uniqueness.
35/// Logs a warning if the generated name differs from the original due to duplicates.
36pub fn generate_unique_node_name(
37    node_name: impl Into<String>,
38    validation_context: Rc<RefCell<ValidationContext>>,
39) -> String {
40    let mut context = validation_context
41        .try_borrow_mut()
42        .expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
43
44    generate_unique_node_name_from_names(node_name, &mut context.used_nodes_names)
45}
46
47/// The longest a node name can be. The kubernetes provider names a node's pod,
48/// hostname and service after it, and a service name is an RFC 1035 label: at
49/// most 63 characters.
50pub const NODE_NAME_MAX_LEN: usize = 63;
51
52/// Put in front of a name that doesn't start with a letter, which an RFC 1035
53/// label must.
54const NODE_NAME_PREFIX: &str = "node-";
55
56/// `name` as a valid RFC 1035 label, the strictest rule any provider applies to
57/// a node's name (a kubernetes service name): lowercase letters, digits and
58/// `-`, starting with a letter and ending with a letter or digit, at most
59/// [`NODE_NAME_MAX_LEN`] characters.
60///
61/// - Uppercase letters are lowercased (`Collator-1000` -> `collator-1000`).
62/// - Any other character becomes `-` (`my_node.1` -> `my-node-1`).
63/// - Leading and trailing `-` are dropped, and a name that then doesn't start
64///   with a letter gets a `node-` prefix (`1st` -> `node-1st`).
65/// - It's cut to [`NODE_NAME_MAX_LEN`].
66///
67/// An empty name stays empty, so it's still reported as one. A valid name is
68/// returned unchanged, so this can be applied any number of times.
69pub fn sanitize_node_name(name: &str) -> String {
70    if name.is_empty() {
71        return String::new();
72    }
73
74    let mapped: String = name
75        .to_lowercase()
76        .chars()
77        .map(|c| {
78            if c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' {
79                c
80            } else {
81                '-'
82            }
83        })
84        .collect();
85
86    let trimmed = mapped.trim_matches('-');
87    let mut sanitized = if trimmed.starts_with(|c: char| c.is_ascii_lowercase()) {
88        trimmed.to_string()
89    } else {
90        format!("{NODE_NAME_PREFIX}{trimmed}")
91    };
92
93    // Only ASCII is left, so cutting at a byte index is cutting at a char.
94    sanitized.truncate(NODE_NAME_MAX_LEN);
95    sanitized.trim_end_matches('-').to_string()
96}
97
98/// `base` with a `-{counter}` suffix, cut so the whole stays within
99/// [`NODE_NAME_MAX_LEN`]. `base` is already sanitized, so it starts with a
100/// letter and is ASCII.
101fn with_counter(base: &str, counter: usize) -> String {
102    let suffix = format!("-{counter}");
103    let keep = NODE_NAME_MAX_LEN
104        .saturating_sub(suffix.len())
105        .min(base.len());
106    format!("{}{suffix}", base[..keep].trim_end_matches('-'))
107}
108
109/// Returns `node_name`, made a valid node name by [`sanitize_node_name`], if it
110/// is not already in `names`.
111///
112/// Otherwise, appends an incrementing `-{counter}` suffix until a unique name is found,
113/// then returns it. Logs a warning when the name had to be adjusted, and when a
114/// duplicate is detected.
115///
116/// Sanitizing comes first, so names that only differ in what is adjusted
117/// (`Alice` and `alice`) are still told apart: the second one is `alice-1`.
118pub fn generate_unique_node_name_from_names(
119    node_name: impl Into<String>,
120    names: &mut HashSet<String>,
121) -> String {
122    let original = node_name.into();
123    let node_name = sanitize_node_name(&original);
124    if node_name != original {
125        warn!(
126            original = %original,
127            adjusted = %node_name,
128            "Node name is not valid on every provider (lowercase letters, digits and '-', \
129             starting with a letter, at most {NODE_NAME_MAX_LEN} characters); using the adjusted name."
130        );
131    }
132
133    if names.insert(node_name.clone()) {
134        return node_name;
135    }
136
137    let mut counter = 1;
138    let mut candidate = node_name.clone();
139    while names.contains(&candidate) {
140        candidate = with_counter(&node_name, counter);
141        counter += 1;
142    }
143
144    warn!(
145        original = %node_name,
146        adjusted = %candidate,
147        "Duplicate node name detected."
148    );
149
150    names.insert(candidate.clone());
151    candidate
152}
153
154pub fn ensure_value_is_not_empty(value: &str) -> Result<(), anyhow::Error> {
155    if value.is_empty() {
156        Err(ValidationError::CantBeEmpty().into())
157    } else {
158        Ok(())
159    }
160}
161
162pub fn ensure_port_unique(
163    port: Port,
164    validation_context: Rc<RefCell<ValidationContext>>,
165) -> Result<(), anyhow::Error> {
166    let mut context = validation_context
167        .try_borrow_mut()
168        .expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
169
170    if !context.used_ports.contains(&port) {
171        context.used_ports.push(port);
172        return Ok(());
173    }
174
175    Err(ValidationError::PortAlreadyUsed(port).into())
176}
177
178pub fn generate_unique_para_id(
179    para_id: ParaId,
180    validation_context: Rc<RefCell<ValidationContext>>,
181) -> String {
182    let mut context = validation_context
183        .try_borrow_mut()
184        .expect(&format!("{BORROWABLE}, {THIS_IS_A_BUG}"));
185
186    if let Some(suffix) = context.used_para_ids.get_mut(&para_id) {
187        *suffix += 1;
188        format!("{para_id}-{suffix}")
189    } else {
190        // insert 0, since will be used next time.
191        context.used_para_ids.insert(para_id, 0);
192        para_id.to_string()
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    /// What a kubernetes service name must be (RFC 1035 label).
201    fn is_rfc1035_label(name: &str) -> bool {
202        let bytes = name.as_bytes();
203        !name.is_empty()
204            && name.len() <= NODE_NAME_MAX_LEN
205            && bytes[0].is_ascii_lowercase()
206            && bytes[bytes.len() - 1].is_ascii_alphanumeric()
207            && bytes
208                .iter()
209                .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || *b == b'-')
210    }
211
212    #[test]
213    fn a_valid_name_is_kept_as_is() {
214        for name in ["alice", "bob-1", "collator-1000", "a"] {
215            assert_eq!(sanitize_node_name(name), name);
216        }
217    }
218
219    #[test]
220    fn an_invalid_name_is_made_a_valid_one() {
221        for (name, expected) in [
222            ("Alice", "alice"),
223            ("Collator-1000", "collator-1000"),
224            ("myNode", "mynode"),
225            ("my_node.1", "my-node-1"),
226            ("my node", "my-node"),
227            ("-alice-", "alice"),
228            ("1st", "node-1st"),
229            ("_", "node"),
230            ("Élan", "lan"),
231        ] {
232            let sanitized = sanitize_node_name(name);
233            assert_eq!(sanitized, expected, "{name}");
234            assert!(is_rfc1035_label(&sanitized), "{name} -> {sanitized}");
235        }
236    }
237
238    #[test]
239    fn a_long_name_is_cut_to_the_limit() {
240        let long = format!("a{}", "-b".repeat(40));
241        let sanitized = sanitize_node_name(&long);
242        assert!(sanitized.len() <= NODE_NAME_MAX_LEN, "{sanitized}");
243        assert!(is_rfc1035_label(&sanitized), "{sanitized}");
244    }
245
246    #[test]
247    fn sanitizing_twice_changes_nothing() {
248        for name in ["Alice", "my_node.1", "1st", "Élan", &"x".repeat(80)] {
249            let once = sanitize_node_name(name);
250            assert_eq!(sanitize_node_name(&once), once, "{name}");
251        }
252    }
253
254    #[test]
255    fn an_empty_name_stays_empty_to_be_reported() {
256        assert_eq!(sanitize_node_name(""), "");
257    }
258
259    #[test]
260    fn names_that_only_differ_in_case_are_still_unique() {
261        let mut names = HashSet::new();
262        assert_eq!(
263            generate_unique_node_name_from_names("Alice", &mut names),
264            "alice"
265        );
266        assert_eq!(
267            generate_unique_node_name_from_names("alice", &mut names),
268            "alice-1"
269        );
270    }
271
272    #[test]
273    fn a_counter_suffix_keeps_a_long_name_within_the_limit() {
274        let mut names = HashSet::new();
275        let long = "n".repeat(NODE_NAME_MAX_LEN);
276        assert_eq!(
277            generate_unique_node_name_from_names(&long, &mut names),
278            long
279        );
280        let second = generate_unique_node_name_from_names(&long, &mut names);
281        assert!(is_rfc1035_label(&second), "{second}");
282        assert!(second.ends_with("-1"), "{second}");
283    }
284}