From de47554aa10c9b3c9776d968aefa773c5acd8e28 Mon Sep 17 00:00:00 2001 From: Tim Simpson Date: Tue, 21 Jan 2025 11:13:40 -0600 Subject: [PATCH] Add support for ACLs and new fields on clusters and networks (#97) * adds commands to work with ACLs * add commands to add public access and ACL ids to a cluster * Update updates to support expanded ACL cidr_block and public_access flag for networks * Update API schemas * runs formatter * fix linter complaints --------- Co-authored-by: Daniel Givens --- api/src/lib.rs | 2 + cli/src/main.rs | 220 +++++++++++++++++++++++++++++- cli/src/v1/infra.rs | 2 +- generated/src/infra/formats.rs | 15 ++ generated/src/infra/operations.rs | 120 ++++++++++++++++ generated/src/infra/schemas.rs | 95 ++++++++++++- generated/src/mesdb/schemas.rs | 12 +- 7 files changed, 458 insertions(+), 8 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index db18be1..b84619b 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -42,6 +42,8 @@ pub use access::GroupId; pub use access::MemberId; pub use access::PolicyId; pub use esc_client_base::Client as EscRequestSender; +pub use infra::Acl; +pub use infra::AclCidrBlock; pub use infra::NetworkId; pub use infra::PeeringId; pub use infra::Provider; diff --git a/cli/src/main.rs b/cli/src/main.rs index 294250a..15e803b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -12,6 +12,7 @@ mod output; mod utils; mod v1; +use cidr::Cidr; use esc_api::resources::MfaStatus; use esc_api::{GroupId, MemberId, OrgId}; use output::OutputFormat; @@ -482,10 +483,98 @@ struct Infra { #[derive(StructOpt, Debug)] enum InfraCommand { + Acls(Acls), Networks(Networks), Peerings(Peerings), } +#[derive(StructOpt, Debug)] +#[structopt(about = "Gathers acls management commands")] +struct Acls { + #[structopt(subcommand)] + acls_command: AclsCommand, +} + +#[derive(StructOpt, Debug)] +enum AclsCommand { + Create(CreateAcl), + Delete(DeleteAcl), + Get(GetAcl), + List(ListAcls), + Update(UpdateAcl), +} + +#[derive(StructOpt, Debug)] +#[structopt(about = "Create an acl")] +struct CreateAcl { + #[structopt(long, parse(try_from_str = parse_org_id), default_value = "", help = "The organization id the acl will relate to")] + org_id: esc_api::resources::OrganizationId, + + #[structopt(long, parse(try_from_str = parse_project_id), default_value = "", help = "The project id the acl will relate to")] + project_id: esc_api::resources::ProjectId, + + #[structopt(long, parse(try_from_str = parse_cidr_input), help = "The CIDR blocks who will have access. Format: \",\"")] + cidr_blocks: Vec, + + #[structopt(long, help = "Human-readable description of the acl")] + description: String, +} + +#[derive(StructOpt, Debug)] +#[structopt(about = "Deletes an acl")] +struct DeleteAcl { + #[structopt(long, parse(try_from_str = parse_org_id), default_value = "", help = "The organization id the acl relates to")] + org_id: esc_api::resources::OrganizationId, + + #[structopt(long, parse(try_from_str = parse_project_id), default_value = "", help = "The project id the acl relates to")] + project_id: esc_api::resources::ProjectId, + + #[structopt(long, short, parse(try_from_str = parse_acl_id), help = "A acl's id")] + id: esc_api::infra::AclId, +} + +#[derive(StructOpt, Debug)] +#[structopt(about = "Reads an acl information")] +struct GetAcl { + #[structopt(long, parse(try_from_str = parse_org_id), default_value = "", help = "The organization id the acl relates to")] + org_id: esc_api::resources::OrganizationId, + + #[structopt(long, parse(try_from_str = parse_project_id), default_value = "", help = "The project id the acl relates to")] + project_id: esc_api::resources::ProjectId, + + #[structopt(long, short, parse(try_from_str = parse_acl_id), help = "An acl's id")] + id: esc_api::infra::AclId, +} + +#[derive(StructOpt, Debug)] +#[structopt(about = "List acls of an organization, given a project")] +struct ListAcls { + #[structopt(long, parse(try_from_str = parse_org_id), default_value = "", help = "The organization id the acls relate to")] + org_id: esc_api::resources::OrganizationId, + + #[structopt(long, parse(try_from_str = parse_project_id), default_value = "", help = "The project id the acls relate to")] + project_id: esc_api::resources::ProjectId, +} + +#[derive(StructOpt, Debug)] +#[structopt(about = "Updates an acl")] +struct UpdateAcl { + #[structopt(long, parse(try_from_str = parse_org_id), default_value = "", help = "The organization id the acl relates to")] + org_id: esc_api::resources::OrganizationId, + + #[structopt(long, parse(try_from_str = parse_project_id), default_value = "", help = "The project id the acl relates to")] + project_id: esc_api::resources::ProjectId, + + #[structopt(long, short, parse(try_from_str = parse_acl_id), help = "An acl's id")] + id: esc_api::infra::AclId, + + #[structopt(long, parse(try_from_str = parse_cidr_input), help = "The CIDR blocks who will have access. Format: \",\"")] + cidr_blocks: Vec, + + #[structopt(long, help = "A human-readable acl's description")] + description: Option, +} + #[derive(StructOpt, Debug)] #[structopt(about = "Gathers networks management commands")] struct Networks { @@ -515,13 +604,19 @@ struct CreateNetwork { provider: esc_api::infra::Provider, #[structopt(long, parse(try_from_str = parse_cidr), help = "Classless Inter-Domain Routing block (CIDR)")] - cidr_block: cidr::Ipv4Cidr, + cidr_block: Option, #[structopt(long, help = "Human-readable description of the network")] description: String, #[structopt(long, help = "Cloud provider region")] region: String, + + #[structopt( + long, + help = "Networks with public access enabled can have clusters with public access enabled, whereas networks without can only be accessed via peering. Defaults to false." + )] + public_access: bool, } #[derive(StructOpt, Debug)] @@ -956,6 +1051,9 @@ struct CreateCluster { )] source_project_id: Option, + #[structopt(long, help = "The ID of an ACL if one is being used")] + acl_id: Option, + #[structopt(long, parse(try_from_str = parse_network_id), help = "The network id the cluster will be set on")] network_id: esc_api::infra::NetworkId, @@ -999,6 +1097,9 @@ struct CreateCluster { #[structopt(long, help = "Throughput in Mb/s for disk (only AWS)")] pub disk_throughput: Option, + #[structopt(long, help = "If set, this cluster will be publicly accessible")] + pub public_access: Option, + #[structopt(long, help = "The protected flag prevents from accidental deletion")] protected: Option, } @@ -1038,6 +1139,9 @@ struct UpdateCluster { #[structopt(long, short, parse(try_from_str = parse_cluster_id), help = "Id of the cluster you want to update")] id: esc_api::ClusterId, + #[structopt(long, help = "The ACL id used by a cluster")] + acl_id: Option, + #[structopt(long, help = "A human-readable description of the cluster")] description: Option, @@ -1533,6 +1637,10 @@ fn parse_project_id(src: &str) -> Result Ok(esc_api::resources::ProjectId(src.to_string())) } +fn parse_acl_id(src: &str) -> Result { + Ok(esc_api::infra::AclId(src.to_string())) +} + fn parse_network_id(src: &str) -> Result { Ok(esc_api::infra::NetworkId(src.to_string())) } @@ -1581,6 +1689,40 @@ fn parse_projection_level(src: &str) -> Result Result { + if s.contains(',') { + let (cidr, comment) = s + .split_once(',') + .ok_or(format!("Invalid CIDR input: {}", s))?; + let cidr = cidr + .parse::() + .map_err(|e| format!("Invalid CIDR input: {}", e))?; + Ok(esc_api::infra::AclCidrBlock { + address: cidr_to_string(cidr), + comment: Some(comment.to_string()), + }) + } else { + let cidr = s + .parse::() + .map_err(|e| format!("Invalid CIDR input: {}", e))?; + Ok(esc_api::infra::AclCidrBlock { + address: cidr_to_string(cidr), + comment: None, + }) + } +} + +// When a CIDR is a host address, the output of Ipv4Cidr::to_string() is a +// single IP address. This is used to ensure that the output remains in CIDR +// notation. +fn cidr_to_string(cidr: cidr::Ipv4Cidr) -> String { + if cidr.is_host_address() { + format!("{}/32", cidr) + } else { + cidr.to_string() + } +} + fn parse_enum(env: &'static HashMap<&'static str, A>, src: &str) -> Result { match env.get(src) { Some(p) => Ok(p.clone()), @@ -1607,7 +1749,7 @@ fn parse_invite_id(src: &str) -> Result { } fn parse_cidr(src: &str) -> Result { - src.parse() + src.parse::() } #[derive(Debug)] @@ -1641,6 +1783,16 @@ impl Printer { } Ok(()) } + + pub fn print_json_only( + &self, + value: A, + ) -> Result<(), Box> { + if self.render_as_v1 { + serde_json::to_writer_pretty(std::io::stdout(), &value)?; + } + Ok(()) + } } pub struct StaticAuthorization { @@ -2121,18 +2273,77 @@ async fn call_api<'a, 'b>( }, Command::Infra(infra) => match infra.infra_command { + InfraCommand::Acls(acls) => match acls.acls_command { + AclsCommand::Create(params) => { + let client = client_builder.create().await?; + let resp = esc_api::infra::create_acl( + &client, + params.org_id, + params.project_id, + esc_api::infra::CreateAclRequest { + cidr_blocks: params.cidr_blocks, + description: params.description, + }, + ) + .await?; + printer.print_json_only(resp)?; + } + AclsCommand::Delete(params) => { + let client = client_builder.create().await?; + esc_api::infra::delete_acl( + &client, + params.org_id, + params.project_id, + params.id, + ) + .await?; + } + AclsCommand::Get(params) => { + let client = client_builder.create().await?; + let resp = esc_api::infra::get_acl( + &client, + params.org_id, + params.project_id, + params.id, + ) + .await?; + printer.print_json_only(resp)?; + } + AclsCommand::List(params) => { + let client = client_builder.create().await?; + let resp = esc_api::infra::list_acls(&client, params.org_id, params.project_id) + .await?; + printer.print_json_only(resp)?; + } + AclsCommand::Update(params) => { + let client = client_builder.create().await?; + esc_api::infra::update_acl( + &client, + params.org_id, + params.project_id, + params.id, + esc_api::infra::UpdateAclRequest { + cidr_blocks: Some(params.cidr_blocks), + description: params.description, + }, + ) + .await?; + } + }, InfraCommand::Networks(networks) => match networks.networks_command { NetworksCommand::Create(params) => { + let cidr_block = params.cidr_block.map(|cidr| cidr.to_string()); let client = client_builder.create().await?; let resp = esc_api::infra::create_network( &client, params.org_id, params.project_id, esc_api::infra::CreateNetworkRequest { - cidr_block: params.cidr_block.to_string(), + cidr_block, description: params.description, provider: params.provider.to_string(), region: params.region, + public_access: params.public_access, }, ) .await?; @@ -2514,6 +2725,7 @@ async fn call_api<'a, 'b>( params.org_id, params.project_id, esc_api::mesdb::CreateClusterRequest { + acl_id: params.acl_id, description: params.description, disk_iops: params.disk_iops, disk_size_gb: params.disk_size_in_gb, @@ -2528,6 +2740,7 @@ async fn call_api<'a, 'b>( source_node_index: None, // TODO: add source_node_index topology: params.topology, protected: params.protected, + public_access: params.public_access, }, ) .await?; @@ -2565,6 +2778,7 @@ async fn call_api<'a, 'b>( params.project_id, params.id, esc_api::mesdb::UpdateClusterRequest { + acl_id: params.acl_id, description: params.description, protected: params.protected, }, diff --git a/cli/src/v1/infra.rs b/cli/src/v1/infra.rs index 40054cd..6ac337a 100644 --- a/cli/src/v1/infra.rs +++ b/cli/src/v1/infra.rs @@ -57,7 +57,7 @@ impl ToV1 for esc_api::infra::Network { type V1Type = Network; fn to_v1(self) -> Self::V1Type { Network { - cidr_block: self.cidr_block, + cidr_block: self.cidr_block.unwrap(), description: self.description, id: self.id, project_id: self.project_id, diff --git a/generated/src/infra/formats.rs b/generated/src/infra/formats.rs index 2e12200..6455714 100644 --- a/generated/src/infra/formats.rs +++ b/generated/src/infra/formats.rs @@ -1,3 +1,18 @@ +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct AclId(pub String); + +impl std::fmt::Display for AclId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +impl AsRef for AclId { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct NetworkId(pub String); diff --git a/generated/src/infra/operations.rs b/generated/src/infra/operations.rs index 2059c5f..5d4187c 100644 --- a/generated/src/infra/operations.rs +++ b/generated/src/infra/operations.rs @@ -6,6 +6,34 @@ use esc_client_base::urlencode; use esc_client_base::Client; use esc_client_base::Result; use reqwest::Method; +/// creates a new acl +/// # Arguments +/// +/// * `organization_id` - The id of the organization +/// * `project_id` - The id of the project +/// * `create_acl_request` +pub async fn create_acl( + client: &Client, + organization_id: OrganizationId, + project_id: ProjectId, + // Describes the new acl + create_acl_request: CreateAclRequest, +) -> Result { + let url = format!( + "/infra/v1/organizations/{organizationId}/projects/{projectId}/acls", + organizationId = urlencode(organization_id), + projectId = urlencode(project_id), + ); + client + .send_request::( + Method::POST, + url, + Some(&create_acl_request), + None, + ) + .await +} + /// creates a new network /// # Arguments /// @@ -90,6 +118,29 @@ pub async fn create_peering_commands( .await } +/// deletes a acl +/// # Arguments +/// +/// * `organization_id` - The id of the organization +/// * `project_id` - The id of the project +/// * `acl_id` - The id of the acl +pub async fn delete_acl( + client: &Client, + organization_id: OrganizationId, + project_id: ProjectId, + acl_id: AclId, +) -> Result<()> { + let url = format!( + "/infra/v1/organizations/{organizationId}/projects/{projectId}/acls/{aclId}", + organizationId = urlencode(organization_id), + projectId = urlencode(project_id), + aclId = urlencode(acl_id), + ); + client + .send_request::<(), ()>(Method::DELETE, url, None, Some(())) + .await +} + /// deletes a network /// # Arguments /// @@ -136,6 +187,29 @@ pub async fn delete_peering( .await } +/// gets a single acl +/// # Arguments +/// +/// * `organization_id` - The id of the organization +/// * `project_id` - The id of the project +/// * `acl_id` - The id of the acl +pub async fn get_acl( + client: &Client, + organization_id: OrganizationId, + project_id: ProjectId, + acl_id: AclId, +) -> Result { + let url = format!( + "/infra/v1/organizations/{organizationId}/projects/{projectId}/acls/{aclId}", + organizationId = urlencode(organization_id), + projectId = urlencode(project_id), + aclId = urlencode(acl_id), + ); + client + .send_request::<(), GetAclResponse>(Method::GET, url, None, None) + .await +} + /// gets a single network /// # Arguments /// @@ -182,6 +256,26 @@ pub async fn get_peering( .await } +/// lists all acls under the given project +/// # Arguments +/// +/// * `organization_id` - The id of the organization +/// * `project_id` - The id of the project +pub async fn list_acls( + client: &Client, + organization_id: OrganizationId, + project_id: ProjectId, +) -> Result { + let url = format!( + "/infra/v1/organizations/{organizationId}/projects/{projectId}/acls", + organizationId = urlencode(organization_id), + projectId = urlencode(project_id), + ); + client + .send_request::<(), ListAclsResponse>(Method::GET, url, None, None) + .await +} + /// lists all networks under the given project /// # Arguments /// @@ -222,6 +316,32 @@ pub async fn list_peerings( .await } +/// updates the given acl +/// # Arguments +/// +/// * `organization_id` - The id of the organization +/// * `project_id` - The id of the project +/// * `acl_id` - The id of the acl +/// * `update_acl_request` +pub async fn update_acl( + client: &Client, + organization_id: OrganizationId, + project_id: ProjectId, + acl_id: AclId, + // describes changes to make to an acl + update_acl_request: UpdateAclRequest, +) -> Result<()> { + let url = format!( + "/infra/v1/organizations/{organizationId}/projects/{projectId}/acls/{aclId}", + organizationId = urlencode(organization_id), + projectId = urlencode(project_id), + aclId = urlencode(acl_id), + ); + client + .send_request::(Method::PUT, url, Some(&update_acl_request), Some(())) + .await +} + /// updates the given network /// # Arguments /// diff --git a/generated/src/infra/schemas.rs b/generated/src/infra/schemas.rs index 822962e..6fcc266 100644 --- a/generated/src/infra/schemas.rs +++ b/generated/src/infra/schemas.rs @@ -2,6 +2,55 @@ use super::formats::*; use crate::resources::formats::ProjectId; use std::collections::HashMap; +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Acl { + pub cidr_blocks: Vec, + pub created: String, + pub description: String, + pub id: AclId, + pub project_id: ProjectId, + pub status: AclStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub updated: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AclCidrBlock { + pub address: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum AclStatus { + Active, + Deleted, +} +impl std::fmt::Display for AclStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AclStatus::Active => write!(f, "active"), + AclStatus::Deleted => write!(f, "deleted"), + } + } +} +impl std::cmp::PartialEq<&str> for AclStatus { + fn eq(&self, other: &&str) -> bool { + match self { + AclStatus::Active => *other == "active", + AclStatus::Deleted => *other == "deleted", + } + } +} +impl std::cmp::PartialEq for &str { + fn eq(&self, other: &AclStatus) -> bool { + other == self + } +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Command { @@ -12,12 +61,27 @@ pub struct Command { pub value: String, } +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateAclRequest { + pub cidr_blocks: Vec, + pub description: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateAclResponse { + pub id: AclId, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateNetworkRequest { - pub provider: String, - pub cidr_block: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub cidr_block: Option, pub description: String, + pub provider: String, + pub public_access: bool, pub region: String, } @@ -60,6 +124,12 @@ pub struct CreatePeeringResponse { pub type Fields = HashMap; +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAclResponse { + pub acl: Acl, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetNetworkResponse { @@ -72,6 +142,12 @@ pub struct GetPeeringResponse { pub peering: Peering, } +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListAclsResponse { + pub acls: Vec, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListNetworksResponse { @@ -87,16 +163,19 @@ pub struct ListPeeringsResponse { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Network { - pub cidr_block: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub cidr_block: Option, pub created: String, pub description: String, pub id: NetworkId, pub project_id: ProjectId, pub provider: String, + pub public_access: bool, pub region: String, pub status: NetworkStatus, } +/// The status of the network #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum NetworkStatus { @@ -153,6 +232,7 @@ pub struct Peering { pub status: PeeringStatus, } +/// The status of the peering #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum PeeringStatus { @@ -228,6 +308,15 @@ impl std::cmp::PartialEq for &str { } } +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateAclRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub cidr_blocks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateNetworkRequest { diff --git a/generated/src/mesdb/schemas.rs b/generated/src/mesdb/schemas.rs index 41909a0..e8263e3 100644 --- a/generated/src/mesdb/schemas.rs +++ b/generated/src/mesdb/schemas.rs @@ -67,6 +67,8 @@ impl std::cmp::PartialEq for &str { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Cluster { + #[serde(skip_serializing_if = "Option::is_none")] + pub acl_id: Option, pub can_expand_disk: bool, pub cloud_integrated_authentication: bool, pub created: DateTime, @@ -85,13 +87,15 @@ pub struct Cluster { pub patch_available: bool, pub project_id: ProjectId, pub projection_level: ProjectionLevel, + pub protected: bool, pub provider: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub public_access: Option, pub region: String, pub server_version: String, pub server_version_tag: String, pub status: ClusterStatus, pub topology: Topology, - pub protected: bool, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -220,6 +224,8 @@ pub struct CreateBackupResponse { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateClusterRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub acl_id: Option, pub description: String, pub disk_size_gb: i32, pub disk_type: String, @@ -239,6 +245,8 @@ pub struct CreateClusterRequest { #[serde(skip_serializing_if = "Option::is_none")] pub protected: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub public_access: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub source_backup_project_id: Option, } @@ -435,6 +443,8 @@ impl std::cmp::PartialEq for &str { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateClusterRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub acl_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")]