From f3ed57f0b9d2adf9bca0bbf69ce3e3f8b769302f Mon Sep 17 00:00:00 2001 From: sinhaashish Date: Mon, 22 Jul 2024 09:12:27 +0000 Subject: [PATCH] feat(topology): add label pool code in plugin Signed-off-by: sinhaashish --- control-plane/plugin/src/lib.rs | 5 + control-plane/plugin/src/resources/error.rs | 77 ++++++++++- control-plane/plugin/src/resources/mod.rs | 18 +++ control-plane/plugin/src/resources/node.rs | 135 ++++---------------- control-plane/plugin/src/resources/pool.rs | 108 +++++++++++++++- control-plane/plugin/src/resources/utils.rs | 99 ++++++++++++++ 6 files changed, 325 insertions(+), 117 deletions(-) diff --git a/control-plane/plugin/src/lib.rs b/control-plane/plugin/src/lib.rs index 29500a2d1..89f559ec1 100644 --- a/control-plane/plugin/src/lib.rs +++ b/control-plane/plugin/src/lib.rs @@ -244,6 +244,11 @@ impl ExecuteOperation for LabelResources { label, overwrite, } => node::Node::label(id, label.to_string(), *overwrite, &cli_args.output).await, + LabelResources::Pool { + id, + label, + overwrite, + } => pool::Pool::label(id, label.to_string(), *overwrite, &cli_args.output).await, } } } diff --git a/control-plane/plugin/src/resources/error.rs b/control-plane/plugin/src/resources/error.rs index 173064735..a2268204a 100644 --- a/control-plane/plugin/src/resources/error.rs +++ b/control-plane/plugin/src/resources/error.rs @@ -1,4 +1,3 @@ -use crate::resources::node; use snafu::Snafu; /// All errors returned when resources command fails. @@ -25,9 +24,14 @@ pub enum Error { source: openapi::tower::client::Error, }, #[snafu(display("Invalid label format: {source}"))] - NodeLabelFormat { source: node::TopologyError }, + NodeLabelFormat { source: TopologyError }, #[snafu(display("{source}"))] - NodeLabel { source: node::OpError }, + NodeLabel { source: OpError }, + #[snafu(display("Invalid label format: {source}"))] + PoolLabelFormat { source: TopologyError }, + #[snafu(display("{source}"))] + PoolLabel { source: OpError }, + /// Error when node uncordon request fails. #[snafu(display("Failed to uncordon node {id}. Error {source}"))] NodeUncordonError { @@ -99,3 +103,70 @@ pub enum Error { ))] LabelNodeFilter { labels: String }, } + +/// Errors related to label topology formats. +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +pub enum TopologyError { + #[snafu(display("key must not be an empty string"))] + KeyIsEmpty {}, + #[snafu(display("value must not be an empty string"))] + ValueIsEmpty {}, + #[snafu(display("key part must no more than 63 characters"))] + KeyTooLong {}, + #[snafu(display("value part must no more than 63 characters"))] + ValueTooLong {}, + #[snafu(display("both key and value parts must start with an ascii alphanumeric character"))] + EdgesNotAlphaNum {}, + #[snafu(display("key can contain at most one / character"))] + KeySlashCount {}, + #[snafu(display( + "only ascii alphanumeric characters and (/ - _ .) are allowed for the key part" + ))] + KeyIsNotAlphaNumericPlus {}, + #[snafu(display( + "only ascii alphanumeric characters and (- _ .) are allowed for the label part" + ))] + ValueIsNotAlphaNumericPlus {}, + #[snafu(display("only a single assignment key=value is allowed"))] + LabelMultiAssign {}, + #[snafu(display( + "the supported formats are: \ + key=value for adding (example: group=a) \ + and key- for removing (example: group-)" + ))] + LabelAssign {}, +} + +/// Errors related to node label topology operation execution. +#[derive(Debug, snafu::Snafu)] +#[snafu(visibility(pub))] +pub enum OpError { + #[snafu(display("{resource} {id} not unlabelled as it did not contain the label"))] + LabelNotFound { resource: String, id: String }, + #[snafu(display("{resource} {id} not labelled as the same label already exists"))] + LabelExists { resource: String, id: String }, + #[snafu(display("{resource} {id} not found"))] + ResourceNotFound { resource: String, id: String }, + #[snafu(display( + "{resource} {id} not labelled as the label key already exists, but with a different value and --overwrite is false" + ))] + LabelConflict { resource: String, id: String }, + #[snafu(display("Failed to label {resource} {id}. Error {source}"))] + Generic { + resource: String, + id: String, + source: openapi::tower::client::Error, + }, +} + +impl From for Error { + fn from(source: TopologyError) -> Self { + Self::NodeLabelFormat { source } + } +} +impl From for Error { + fn from(source: OpError) -> Self { + Self::NodeLabel { source } + } +} diff --git a/control-plane/plugin/src/resources/mod.rs b/control-plane/plugin/src/resources/mod.rs index 30d40dc2e..6b2185a60 100644 --- a/control-plane/plugin/src/resources/mod.rs +++ b/control-plane/plugin/src/resources/mod.rs @@ -146,6 +146,24 @@ pub enum LabelResources { #[clap(long)] overwrite: bool, }, + /// Adds or removes a label to or from the specified pool. + Pool { + /// The id of the pool to label/unlabel. + id: PoolId, + /// The label to be added or removed from the pool. + /// To add a label, please use the following format: + /// ${key}=${value} + /// To remove a label, please use the following format: + /// ${key}- + /// A label key and value must begin with a letter or number, and may contain letters, + /// numbers, hyphens, dots, and underscores, up to 63 characters each. + /// The key may contain a single slash. + label: String, + /// Allow labels to be overwritten, otherwise reject label updates that overwrite existing + /// labels. + #[clap(long)] + overwrite: bool, + }, } #[derive(clap::Subcommand, Debug)] diff --git a/control-plane/plugin/src/resources/node.rs b/control-plane/plugin/src/resources/node.rs index 2e82875bc..bf935c1ea 100644 --- a/control-plane/plugin/src/resources/node.rs +++ b/control-plane/plugin/src/resources/node.rs @@ -1,9 +1,10 @@ use crate::{ operations::{Cordoning, Drain, GetWithArgs, Label, ListWithArgs, PluginResult}, resources::{ - error::Error, + error::{Error, LabelAssignSnafu, OpError, TopologyError}, utils::{ - self, optional_cell, print_table, CreateRow, CreateRows, GetHeaderRow, OutputFormat, + self, optional_cell, print_table, validate_topology_key, validate_topology_value, + CreateRow, CreateRows, GetHeaderRow, OutputFormat, }, NodeId, }, @@ -677,109 +678,6 @@ impl Drain for Node { } } -/// Errors related to node label topology formats. -#[derive(Debug, snafu::Snafu)] -pub enum TopologyError { - #[snafu(display("key must not be an empty string"))] - KeyIsEmpty {}, - #[snafu(display("value must not be an empty string"))] - ValueIsEmpty {}, - #[snafu(display("key part must no more than 63 characters"))] - KeyTooLong {}, - #[snafu(display("value part must no more than 63 characters"))] - ValueTooLong {}, - #[snafu(display("both key and value parts must start with an ascii alphanumeric character"))] - EdgesNotAlphaNum {}, - #[snafu(display("key can contain at most one / character"))] - KeySlashCount {}, - #[snafu(display( - "only ascii alphanumeric characters and (/ - _ .) are allowed for the key part" - ))] - KeyIsNotAlphaNumericPlus {}, - #[snafu(display( - "only ascii alphanumeric characters and (- _ .) are allowed for the label part" - ))] - ValueIsNotAlphaNumericPlus {}, - #[snafu(display("only a single assignment key=value is allowed"))] - LabelMultiAssign {}, - #[snafu(display( - "the supported formats are: \ - key=value for adding (example: group=a) \ - and key- for removing (example: group-)" - ))] - LabelAssign {}, -} - -/// Errors related to node label topology operation execution. -#[derive(Debug, snafu::Snafu)] -pub enum OpError { - #[snafu(display("Node {id} not unlabelled as it did not contain the label"))] - LabelNotFound { id: String }, - #[snafu(display("Node {id} not labelled as the same label already exists"))] - LabelExists { id: String }, - #[snafu(display("Node {id} not found"))] - NodeNotFound { id: String }, - #[snafu(display( - "Node {id} not labelled as the label key already exists, but with a different value and --overwrite is false" - ))] - LabelConflict { id: String }, - #[snafu(display("Failed to label node {id}. Error {source}"))] - Generic { - id: String, - source: openapi::tower::client::Error, - }, -} - -impl From for Error { - fn from(source: TopologyError) -> Self { - Self::NodeLabelFormat { source } - } -} -impl From for Error { - fn from(source: OpError) -> Self { - Self::NodeLabel { source } - } -} - -fn allowed_topology_chars(key: char) -> bool { - key.is_ascii_alphanumeric() || matches!(key, '_' | '-' | '.') -} -fn allowed_topology_tips(label: &str) -> bool { - fn allowed_topology_tips_chars(char: Option) -> bool { - char.map(|c| c.is_ascii_alphanumeric()).unwrap_or(true) - } - - allowed_topology_tips_chars(label.chars().next()) - && allowed_topology_tips_chars(label.chars().last()) -} -fn validate_topology_key(key: &str) -> Result<(), TopologyError> { - snafu::ensure!(!key.is_empty(), KeyIsEmptySnafu); - snafu::ensure!(key.len() <= 63, KeyTooLongSnafu); - snafu::ensure!(allowed_topology_tips(key), EdgesNotAlphaNumSnafu); - - snafu::ensure!( - key.chars().filter(|c| c == &'/').count() <= 1, - KeySlashCountSnafu - ); - - snafu::ensure!( - key.chars().all(|c| allowed_topology_chars(c) || c == '/'), - KeyIsNotAlphaNumericPlusSnafu - ); - - Ok(()) -} -fn validate_topology_value(value: &str) -> Result<(), TopologyError> { - snafu::ensure!(!value.is_empty(), ValueIsEmptySnafu); - snafu::ensure!(value.len() <= 63, ValueTooLongSnafu); - snafu::ensure!(allowed_topology_tips(value), EdgesNotAlphaNumSnafu); - snafu::ensure!( - value.chars().all(allowed_topology_chars), - ValueIsNotAlphaNumericPlusSnafu - ); - Ok(()) -} - #[async_trait(?Send)] impl Label for Node { type ID = NodeId; @@ -803,15 +701,25 @@ impl Label for Node { { Err(source) => match source.status() { Some(StatusCode::UNPROCESSABLE_ENTITY) if output.none() => { - Err(OpError::LabelExists { id: id.to_string() }) + Err(OpError::LabelExists { + resource: "Node".to_string(), + id: id.to_string(), + }) } Some(StatusCode::PRECONDITION_FAILED) if output.none() => { - Err(OpError::LabelConflict { id: id.to_string() }) + Err(OpError::LabelConflict { + resource: "Node".to_string(), + id: id.to_string(), + }) } Some(StatusCode::NOT_FOUND) if output.none() => { - Err(OpError::NodeNotFound { id: id.to_string() }) + Err(OpError::ResourceNotFound { + resource: "Node".to_string(), + id: id.to_string(), + }) } _ => Err(OpError::Generic { + resource: "Node".to_string(), id: id.to_string(), source, }), @@ -829,12 +737,19 @@ impl Label for Node { { Err(source) => match source.status() { Some(StatusCode::PRECONDITION_FAILED) if output.none() => { - Err(OpError::LabelNotFound { id: id.to_string() }) + Err(OpError::LabelNotFound { + resource: "Node".to_string(), + id: id.to_string(), + }) } Some(StatusCode::NOT_FOUND) if output.none() => { - Err(OpError::NodeNotFound { id: id.to_string() }) + Err(OpError::ResourceNotFound { + resource: "Node".to_string(), + id: id.to_string(), + }) } _ => Err(OpError::Generic { + resource: "Node".to_string(), id: id.to_string(), source, }), diff --git a/control-plane/plugin/src/resources/pool.rs b/control-plane/plugin/src/resources/pool.rs index 037888870..d63e57d70 100644 --- a/control-plane/plugin/src/resources/pool.rs +++ b/control-plane/plugin/src/resources/pool.rs @@ -1,15 +1,20 @@ use crate::{ - operations::{Get, ListWithArgs, PluginResult}, + operations::{Get, Label, ListWithArgs, PluginResult}, resources::{ - error::Error, + error::{Error, LabelAssignSnafu, OpError, TopologyError}, utils, - utils::{CreateRow, GetHeaderRow}, + utils::{ + optional_cell, print_table, validate_topology_key, validate_topology_value, CreateRow, + GetHeaderRow, OutputFormat, + }, NodeId, PoolId, }, rest_wrapper::RestClient, }; use async_trait::async_trait; +use openapi::apis::StatusCode; use prettytable::Row; +use snafu::ResultExt; use std::collections::HashMap; use super::VolumeId; @@ -50,7 +55,7 @@ impl CreateRow for openapi::models::Pool { ::utils::bytes::into_human(state.capacity), ::utils::bytes::into_human(state.used), ::utils::bytes::into_human(free), - utils::optional_cell(state.committed.map(::utils::bytes::into_human)), + optional_cell(state.committed.map(::utils::bytes::into_human)), ] } } @@ -179,3 +184,98 @@ pub(crate) fn labels_matched( } Ok(true) } + +#[async_trait(?Send)] +impl Label for Pool { + type ID = PoolId; + async fn label( + id: &Self::ID, + label: String, + overwrite: bool, + output: &utils::OutputFormat, + ) -> PluginResult { + let result = if label.contains('=') { + let [key, value] = label.split('=').collect::>()[..] else { + return Err(TopologyError::LabelMultiAssign {}.into()); + }; + + validate_topology_key(key).context(super::error::PoolLabelFormatSnafu)?; + validate_topology_value(value).context(super::error::PoolLabelFormatSnafu)?; + match RestClient::client() + .pools_api() + .put_pool_label(id, key, value, Some(overwrite)) + .await + { + Err(source) => match source.status() { + Some(StatusCode::UNPROCESSABLE_ENTITY) if output.none() => { + Err(OpError::LabelExists { + resource: "Pool".to_string(), + id: id.to_string(), + }) + } + Some(StatusCode::PRECONDITION_FAILED) if output.none() => { + Err(OpError::LabelConflict { + resource: "Pool".to_string(), + id: id.to_string(), + }) + } + Some(StatusCode::NOT_FOUND) if output.none() => { + Err(OpError::ResourceNotFound { + resource: "Pool".to_string(), + id: id.to_string(), + }) + } + _ => Err(OpError::Generic { + resource: "Pool".to_string(), + id: id.to_string(), + source, + }), + }, + Ok(pool) => Ok(pool), + } + } else { + snafu::ensure!(label.len() >= 2 && label.ends_with('-'), LabelAssignSnafu); + let key = &label[.. label.len() - 1]; + validate_topology_key(key)?; + match RestClient::client() + .pools_api() + .del_pool_label(id, key) + .await + { + Err(source) => match source.status() { + Some(StatusCode::PRECONDITION_FAILED) if output.none() => { + Err(OpError::LabelNotFound { + resource: "Pool".to_string(), + id: id.to_string(), + }) + } + Some(StatusCode::NOT_FOUND) if output.none() => { + Err(OpError::ResourceNotFound { + resource: "Pool".to_string(), + id: id.to_string(), + }) + } + _ => Err(OpError::Generic { + resource: "Pool".to_string(), + id: id.to_string(), + source, + }), + }, + Ok(pool) => Ok(pool), + } + }?; + let pool = result.into_body(); + match output { + OutputFormat::Yaml | OutputFormat::Json => { + // Print json or yaml based on output format. + print_table(output, pool); + } + OutputFormat::None => { + // In case the output format is not specified, show a success message. + let labels = pool.spec.unwrap().labels.unwrap_or_default(); + println!("Pool {id} labelled successfully. Current labels: {labels:?}"); + } + } + Ok(()) + } +} diff --git a/control-plane/plugin/src/resources/utils.rs b/control-plane/plugin/src/resources/utils.rs index aaa165d27..de1d43836 100644 --- a/control-plane/plugin/src/resources/utils.rs +++ b/control-plane/plugin/src/resources/utils.rs @@ -1,3 +1,8 @@ +use crate::resources::error::{ + EdgesNotAlphaNumSnafu, KeyIsEmptySnafu, KeyIsNotAlphaNumericPlusSnafu, KeySlashCountSnafu, + KeyTooLongSnafu, TopologyError, ValueIsEmptySnafu, ValueIsNotAlphaNumericPlusSnafu, + ValueTooLongSnafu, +}; use prettytable::{format, Row, Table}; use serde::ser; @@ -202,3 +207,97 @@ where } } } + +/// Checks if a given character is allowed in topology keys. +/// +/// # Description +/// This function determines if a provided character is permissible for use in topology keys. +/// The allowed characters are: +/// - ASCII alphanumeric characters (letters and digits) +/// - Special characters: underscore (`_`), hyphen (`-`), and period (`.`). +/// +/// # Parameters +/// - `key`: A `char` representing the character to check. +/// +/// # Returns +/// Returns `true` if the character is allowed in topology keys; otherwise, returns `false`. +/// +/// # Examples +/// ``` +/// assert_eq!(allowed_topology_chars('a'), true); // ASCII letter +/// assert_eq!(allowed_topology_chars('1'), true); // ASCII digit +/// assert_eq!(allowed_topology_chars('_'), true); // Underscore +/// assert_eq!(allowed_topology_chars('-'), true); // Hyphen +/// assert_eq!(allowed_topology_chars('.'), true); // Period +/// assert_eq!(allowed_topology_chars('!'), false); // Not allowed +/// assert_eq!(allowed_topology_chars(' '), false); // Space is not allowed +/// assert_eq!(allowed_topology_chars('@'), false); // Special character not allowed +/// ``` +pub fn allowed_topology_chars(key: char) -> bool { + key.is_ascii_alphanumeric() || matches!(key, '_' | '-' | '.') +} + +/// Determines if the provided label string has allowed topology tips. +/// +/// # Description +/// This function checks whether the first and last characters of a given string (`label`) are valid +/// for use as topology tips. A valid character for the tips of the string is an ASCII alphanumeric +/// character. If the string is empty, it will return `true`. +/// +/// Internally, this function uses a helper function to verify the first and last characters +/// by mapping them to their alphanumeric status or defaulting to `true` if the character is `None`. +pub fn allowed_topology_tips(label: &str) -> bool { + fn allowed_topology_tips_chars(char: Option) -> bool { + char.map(|c| c.is_ascii_alphanumeric()).unwrap_or(true) + } + + allowed_topology_tips_chars(label.chars().next()) + && allowed_topology_tips_chars(label.chars().last()) +} + +/// Validates a topology key based on specific criteria. +/// +/// # Description +/// This function validates a given topology key string to ensure it adheres to a set of predefined +/// rules. These rules include checks for the key's length, content, and structure. The key is +/// considered valid if: +/// - It is not empty. +/// - It does not exceed 63 characters in length. +/// - The first and last characters are ASCII alphanumeric. +/// - It contains at most one slash ('/') character. +/// - All characters are ASCII alphanumeric, underscore ('_'), hyphen ('-'), period ('.'), or a +/// slash ('/'). +/// +/// If any of these conditions are not met, the function returns an appropriate `TopologyError`. +/// +/// # Parameters +/// - `key`: A `&str` reference representing the topology key to be validated. +pub fn validate_topology_key(key: &str) -> Result<(), TopologyError> { + snafu::ensure!(!key.is_empty(), KeyIsEmptySnafu); + snafu::ensure!(key.len() <= 63, KeyTooLongSnafu); + snafu::ensure!(allowed_topology_tips(key), EdgesNotAlphaNumSnafu); + + snafu::ensure!( + key.chars().filter(|c| c == &'/').count() <= 1, + KeySlashCountSnafu + ); + + snafu::ensure!( + key.chars().all(|c| allowed_topology_chars(c) || c == '/'), + KeyIsNotAlphaNumericPlusSnafu + ); + + Ok(()) +} + +/// Validates a topology value based on specific criteria. +pub fn validate_topology_value(value: &str) -> Result<(), TopologyError> { + snafu::ensure!(!value.is_empty(), ValueIsEmptySnafu); + snafu::ensure!(value.len() <= 63, ValueTooLongSnafu); + snafu::ensure!(allowed_topology_tips(value), EdgesNotAlphaNumSnafu); + snafu::ensure!( + value.chars().all(allowed_topology_chars), + ValueIsNotAlphaNumericPlusSnafu + ); + Ok(()) +}