Skip to content

Commit 9eff233

Browse files
feat(cli): add AWS PrivateLink human-friendly commands (#407)
Add typed CLI commands for AWS PrivateLink connectivity operations, completing 100% CLI coverage for Cloud API. Commands added: - redisctl cloud connectivity privatelink get - redisctl cloud connectivity privatelink create - redisctl cloud connectivity privatelink add-principal - redisctl cloud connectivity privatelink remove-principal - redisctl cloud connectivity privatelink get-script All commands support both standard and Active-Active (CRDB) configurations via optional --region flag. Examples: # Get PrivateLink configuration redisctl cloud connectivity privatelink get --subscription 123 # Create PrivateLink redisctl cloud connectivity privatelink create --subscription 123 \ '{"shareName":"my-share","principal":"123456789012","type":"aws_account"}' # Active-Active variant redisctl cloud connectivity privatelink get --subscription 123 --region 1 All tests passing, clippy clean, formatted.
1 parent c795fce commit 9eff233

File tree

3 files changed

+285
-0
lines changed

3 files changed

+285
-0
lines changed

crates/redisctl/src/cli.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,9 @@ pub enum CloudConnectivityCommands {
405405
/// Transit Gateway operations
406406
#[command(subcommand, name = "tgw")]
407407
Tgw(TgwCommands),
408+
/// AWS PrivateLink operations
409+
#[command(subcommand, name = "privatelink")]
410+
PrivateLink(PrivateLinkCommands),
408411
}
409412

410413
/// VPC Peering Commands
@@ -810,6 +813,67 @@ pub enum TgwCommands {
810813
},
811814
}
812815

816+
/// AWS PrivateLink Commands
817+
#[derive(Subcommand, Debug)]
818+
pub enum PrivateLinkCommands {
819+
/// Get PrivateLink configuration
820+
Get {
821+
/// Subscription ID
822+
#[arg(long)]
823+
subscription: i32,
824+
/// Region ID (for Active-Active databases)
825+
#[arg(long)]
826+
region: Option<i32>,
827+
},
828+
/// Create PrivateLink
829+
Create {
830+
/// Subscription ID
831+
#[arg(long)]
832+
subscription: i32,
833+
/// Region ID (for Active-Active databases)
834+
#[arg(long)]
835+
region: Option<i32>,
836+
/// Configuration JSON file or string (use @filename for file)
837+
data: String,
838+
#[command(flatten)]
839+
async_ops: crate::commands::cloud::async_utils::AsyncOperationArgs,
840+
},
841+
/// Add principals to PrivateLink
842+
#[command(name = "add-principal")]
843+
AddPrincipal {
844+
/// Subscription ID
845+
#[arg(long)]
846+
subscription: i32,
847+
/// Region ID (for Active-Active databases)
848+
#[arg(long)]
849+
region: Option<i32>,
850+
/// Configuration JSON file or string (use @filename for file)
851+
data: String,
852+
},
853+
/// Remove principals from PrivateLink
854+
#[command(name = "remove-principal")]
855+
RemovePrincipal {
856+
/// Subscription ID
857+
#[arg(long)]
858+
subscription: i32,
859+
/// Region ID (for Active-Active databases)
860+
#[arg(long)]
861+
region: Option<i32>,
862+
/// Configuration JSON file or string (use @filename for file)
863+
data: String,
864+
},
865+
/// Get VPC endpoint creation script
866+
#[command(name = "get-script")]
867+
GetScript {
868+
/// Subscription ID
869+
#[arg(long)]
870+
subscription: i32,
871+
/// Region ID (for Active-Active databases)
872+
#[arg(long)]
873+
region: Option<i32>,
874+
},
875+
}
876+
813877
/// Cloud Task Commands
814878
#[derive(Subcommand, Debug)]
815879
pub enum CloudTaskCommands {

crates/redisctl/src/commands/cloud/connectivity/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
#![allow(dead_code)]
44

5+
pub mod private_link;
56
pub mod psc;
67
pub mod tgw;
78
pub mod vpc_peering;
@@ -48,5 +49,15 @@ pub async fn handle_connectivity_command(
4849
CloudConnectivityCommands::Tgw(tgw_cmd) => {
4950
tgw::handle_tgw_command(conn_mgr, profile_name, tgw_cmd, output_format, query).await
5051
}
52+
CloudConnectivityCommands::PrivateLink(pl_cmd) => {
53+
private_link::handle_private_link_command(
54+
conn_mgr,
55+
profile_name,
56+
pl_cmd,
57+
output_format,
58+
query,
59+
)
60+
.await
61+
}
5162
}
5263
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
//! AWS PrivateLink command implementations
2+
3+
#![allow(dead_code)]
4+
5+
use super::ConnectivityOperationParams;
6+
use crate::cli::{OutputFormat, PrivateLinkCommands};
7+
use crate::commands::cloud::async_utils::handle_async_response;
8+
use crate::commands::cloud::utils::{handle_output, print_formatted_output, read_file_input};
9+
use crate::connection::ConnectionManager;
10+
use crate::error::Result as CliResult;
11+
use anyhow::Context;
12+
use redis_cloud::PrivateLinkHandler;
13+
use serde_json::Value;
14+
15+
/// Handle PrivateLink commands
16+
pub async fn handle_private_link_command(
17+
conn_mgr: &ConnectionManager,
18+
profile_name: Option<&str>,
19+
command: &PrivateLinkCommands,
20+
output_format: OutputFormat,
21+
query: Option<&str>,
22+
) -> CliResult<()> {
23+
let client = conn_mgr.create_cloud_client(profile_name).await?;
24+
let handler = PrivateLinkHandler::new(client.clone());
25+
26+
match command {
27+
PrivateLinkCommands::Get {
28+
subscription,
29+
region,
30+
} => handle_get(&handler, *subscription, *region, output_format, query).await,
31+
PrivateLinkCommands::Create {
32+
subscription,
33+
region,
34+
data,
35+
async_ops,
36+
} => {
37+
let params = ConnectivityOperationParams {
38+
conn_mgr,
39+
profile_name,
40+
client: &client,
41+
subscription_id: *subscription,
42+
async_ops,
43+
output_format,
44+
query,
45+
};
46+
handle_create(&handler, &params, *region, data).await
47+
}
48+
PrivateLinkCommands::AddPrincipal {
49+
subscription,
50+
region,
51+
data,
52+
} => {
53+
handle_add_principal(&handler, *subscription, *region, data, output_format, query).await
54+
}
55+
PrivateLinkCommands::RemovePrincipal {
56+
subscription,
57+
region,
58+
data,
59+
} => {
60+
handle_remove_principal(&handler, *subscription, *region, data, output_format, query)
61+
.await
62+
}
63+
PrivateLinkCommands::GetScript {
64+
subscription,
65+
region,
66+
} => handle_get_script(&handler, *subscription, *region, output_format, query).await,
67+
}
68+
}
69+
70+
/// Get PrivateLink configuration
71+
async fn handle_get(
72+
handler: &PrivateLinkHandler,
73+
subscription_id: i32,
74+
region_id: Option<i32>,
75+
output_format: OutputFormat,
76+
query: Option<&str>,
77+
) -> CliResult<()> {
78+
let result = if let Some(region) = region_id {
79+
handler
80+
.get_active_active(subscription_id, region)
81+
.await
82+
.context("Failed to get Active-Active PrivateLink configuration")?
83+
} else {
84+
handler
85+
.get(subscription_id)
86+
.await
87+
.context("Failed to get PrivateLink configuration")?
88+
};
89+
90+
let data = handle_output(result, output_format, query)?;
91+
print_formatted_output(data, output_format)?;
92+
Ok(())
93+
}
94+
95+
/// Create PrivateLink
96+
async fn handle_create(
97+
handler: &PrivateLinkHandler,
98+
params: &ConnectivityOperationParams<'_>,
99+
region_id: Option<i32>,
100+
data: &str,
101+
) -> CliResult<()> {
102+
let content = read_file_input(data)?;
103+
let request: Value = serde_json::from_str(&content).context("Failed to parse JSON input")?;
104+
105+
let result = if let Some(region) = region_id {
106+
handler
107+
.create_active_active(params.subscription_id, region, request)
108+
.await
109+
.context("Failed to create Active-Active PrivateLink")?
110+
} else {
111+
handler
112+
.create(params.subscription_id, request)
113+
.await
114+
.context("Failed to create PrivateLink")?
115+
};
116+
117+
handle_async_response(
118+
params.conn_mgr,
119+
params.profile_name,
120+
result,
121+
params.async_ops,
122+
params.output_format,
123+
params.query,
124+
"Create PrivateLink",
125+
)
126+
.await
127+
}
128+
129+
/// Add principals to PrivateLink
130+
async fn handle_add_principal(
131+
handler: &PrivateLinkHandler,
132+
subscription_id: i32,
133+
region_id: Option<i32>,
134+
data: &str,
135+
output_format: OutputFormat,
136+
query: Option<&str>,
137+
) -> CliResult<()> {
138+
let content = read_file_input(data)?;
139+
let request: Value = serde_json::from_str(&content).context("Failed to parse JSON input")?;
140+
141+
let result = if let Some(region) = region_id {
142+
handler
143+
.add_principals_active_active(subscription_id, region, request)
144+
.await
145+
.context("Failed to add principals to Active-Active PrivateLink")?
146+
} else {
147+
handler
148+
.add_principals(subscription_id, request)
149+
.await
150+
.context("Failed to add principals to PrivateLink")?
151+
};
152+
153+
let data = handle_output(result, output_format, query)?;
154+
print_formatted_output(data, output_format)?;
155+
Ok(())
156+
}
157+
158+
/// Remove principals from PrivateLink
159+
async fn handle_remove_principal(
160+
handler: &PrivateLinkHandler,
161+
subscription_id: i32,
162+
region_id: Option<i32>,
163+
data: &str,
164+
output_format: OutputFormat,
165+
query: Option<&str>,
166+
) -> CliResult<()> {
167+
let content = read_file_input(data)?;
168+
let request: Value = serde_json::from_str(&content).context("Failed to parse JSON input")?;
169+
170+
let result = if let Some(region) = region_id {
171+
handler
172+
.remove_principals_active_active(subscription_id, region, request)
173+
.await
174+
.context("Failed to remove principals from Active-Active PrivateLink")?
175+
} else {
176+
handler
177+
.remove_principals(subscription_id, request)
178+
.await
179+
.context("Failed to remove principals from PrivateLink")?
180+
};
181+
182+
let data = handle_output(result, output_format, query)?;
183+
print_formatted_output(data, output_format)?;
184+
Ok(())
185+
}
186+
187+
/// Get VPC endpoint creation script
188+
async fn handle_get_script(
189+
handler: &PrivateLinkHandler,
190+
subscription_id: i32,
191+
region_id: Option<i32>,
192+
output_format: OutputFormat,
193+
query: Option<&str>,
194+
) -> CliResult<()> {
195+
let result = if let Some(region) = region_id {
196+
handler
197+
.get_endpoint_script_active_active(subscription_id, region)
198+
.await
199+
.context("Failed to get Active-Active endpoint script")?
200+
} else {
201+
handler
202+
.get_endpoint_script(subscription_id)
203+
.await
204+
.context("Failed to get endpoint script")?
205+
};
206+
207+
let data = handle_output(result, output_format, query)?;
208+
print_formatted_output(data, output_format)?;
209+
Ok(())
210+
}

0 commit comments

Comments
 (0)