Skip to content

Commit

Permalink
feat(topology): add label pool code in plugin
Browse files Browse the repository at this point in the history
Signed-off-by: sinhaashish <[email protected]>
  • Loading branch information
sinhaashish committed Jul 29, 2024
1 parent 6979b4f commit 043631c
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 112 deletions.
5 changes: 5 additions & 0 deletions control-plane/plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
80 changes: 77 additions & 3 deletions control-plane/plugin/src/resources/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::resources::node;
use snafu::Snafu;

/// All errors returned when resources command fails.
Expand All @@ -25,9 +24,14 @@ pub enum Error {
source: openapi::tower::client::Error<openapi::models::RestJsonError>,
},
#[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 {
Expand Down Expand Up @@ -99,3 +103,73 @@ 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("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"
))]
PoolNotFound { id: String },
#[snafu(display(
"Pool {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<openapi::models::RestJsonError>,
},
}

impl From<TopologyError> for Error {
fn from(source: TopologyError) -> Self {
Self::NodeLabelFormat { source }
}
}
impl From<OpError> for Error {
fn from(source: OpError) -> Self {
Self::NodeLabel { source }
}
}
18 changes: 18 additions & 0 deletions control-plane/plugin/src/resources/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
108 changes: 3 additions & 105 deletions control-plane/plugin/src/resources/node.rs
Original file line number Diff line number Diff line change
@@ -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,
},
Expand Down Expand Up @@ -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<openapi::models::RestJsonError>,
},
}

impl From<TopologyError> for Error {
fn from(source: TopologyError) -> Self {
Self::NodeLabelFormat { source }
}
}
impl From<OpError> 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<char>) -> 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;
Expand Down
91 changes: 87 additions & 4 deletions control-plane/plugin/src/resources/pool.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)),
]
}
}
Expand Down Expand Up @@ -179,3 +184,81 @@ 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::<Vec<_>>()[..] 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 { id: id.to_string() })
}
Some(StatusCode::PRECONDITION_FAILED) if output.none() => {
Err(OpError::LabelConflict { id: id.to_string() })
}
Some(StatusCode::NOT_FOUND) if output.none() => {
Err(OpError::PoolNotFound { id: id.to_string() })
}
_ => Err(OpError::Generic {
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 { id: id.to_string() })
}
Some(StatusCode::NOT_FOUND) if output.none() => {
Err(OpError::PoolNotFound { id: id.to_string() })
}
_ => Err(OpError::Generic {
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(())
}
}
Loading

0 comments on commit 043631c

Please sign in to comment.