Add a new client / server command to rename CIDR (#310)

* Add a new client / server command to rename CIDR.

* Add a docker test case

* Apply suggestions from code review

Co-authored-by: Matěj Laitl <matej@laitl.cz>
Co-authored-by: Jake McGinty <me@jakebot.org>

---------

Co-authored-by: Matěj Laitl <matej@laitl.cz>
Co-authored-by: Jake McGinty <me@jakebot.org>
pull/312/head
Ryo Kawaguchi 2024-04-23 06:12:36 +09:00 committed by GitHub
parent 8ab0989f8f
commit 3c69de4e4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 206 additions and 5 deletions

View File

@ -12,7 +12,7 @@ use shared::{
AddCidrOpts, AddDeleteAssociationOpts, AddPeerOpts, Association, AssociationContents, Cidr,
CidrTree, DeleteCidrOpts, EnableDisablePeerOpts, Endpoint, EndpointContents, InstallOpts,
Interface, IoErrorContext, ListenPortOpts, NatOpts, NetworkOpts, OverrideEndpointOpts, Peer,
RedeemContents, RenamePeerOpts, State, WrappedIoError, REDEEM_TRANSITION_WAIT,
RedeemContents, RenameCidrOpts, RenamePeerOpts, State, WrappedIoError, REDEEM_TRANSITION_WAIT,
};
use std::{
fmt, io,
@ -193,6 +193,19 @@ enum Command {
sub_opts: AddCidrOpts,
},
/// Rename a CIDR
///
/// By default, you'll be prompted interactively to select a CIDR, but you can
/// also specify all the options in the command, eg:
///
/// --name 'group' --new-name 'family'
RenameCidr {
interface: Interface,
#[clap(flatten)]
sub_opts: RenameCidrOpts,
},
/// Delete a CIDR
DeleteCidr {
interface: Interface,
@ -724,6 +737,36 @@ fn add_cidr(interface: &InterfaceName, opts: &Opts, sub_opts: AddCidrOpts) -> Re
Ok(())
}
fn rename_cidr(
interface: &InterfaceName,
opts: &Opts,
sub_opts: RenameCidrOpts,
) -> Result<(), Error> {
let InterfaceConfig { server, .. } =
InterfaceConfig::from_interface(&opts.config_dir, interface)?;
let api = Api::new(&server);
log::info!("Fetching CIDRs");
let cidrs: Vec<Cidr> = api.http("GET", "/admin/cidrs")?;
if let Some((cidr_request, old_name)) = prompts::rename_cidr(&cidrs, &sub_opts)? {
log::info!("Renaming CIDR...");
let id = cidrs
.iter()
.find(|c| c.name == old_name)
.ok_or_else(|| anyhow!("CIDR not found."))?
.id;
api.http_form("PUT", &format!("/admin/cidrs/{id}"), cidr_request)?;
log::info!("CIDR renamed.");
} else {
log::info!("Exited without renaming CIDR.");
}
Ok(())
}
fn delete_cidr(
interface: &InterfaceName,
opts: &Opts,
@ -1251,6 +1294,10 @@ fn run(opts: &Opts) -> Result<(), Error> {
interface,
sub_opts,
} => add_cidr(&interface, opts, sub_opts)?,
Command::RenameCidr {
interface,
sub_opts,
} => rename_cidr(&interface, opts, sub_opts)?,
Command::DeleteCidr {
interface,
sub_opts,

View File

@ -201,6 +201,30 @@ test_simultaneous_redemption() {
cmd docker exec "$PEER2_CONTAINER" ping -c3 10.66.1.1
}
test_rename_cidr() {
info "Renaming CIDR from peer1"
cmd docker exec "$PEER1_CONTAINER" innernet \
rename-cidr evilcorp \
--name "robots" \
--new-name "coolbeans" \
--yes
sleep 5
info "Confirming the CIDR rename from peer1"
cmd docker exec "$PEER1_CONTAINER" innernet list-cidrs evilcorp | grep coolbeans
info "Renaming CIDR from server"
cmd docker exec "$SERVER_CONTAINER" innernet-server \
rename-cidr evilcorp \
--name "coolbeans" \
--new-name "robots" \
--yes
sleep 5
info "Confirming the CIDR rename from peer1"
cmd docker exec "$PEER1_CONTAINER" innernet list-cidrs evilcorp | grep robots
}
# Run tests (functions prefixed with test_) in alphabetical order.
# Optional filter provided by positional arguments is applied.
for func in $(declare -F | awk '{print $3}'); do

View File

@ -19,6 +19,11 @@ pub async fn routes(
let form = form_body(req).await?;
handlers::create(form, session).await
},
(&Method::PUT, Some(id)) => {
let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?;
let form = form_body(req).await?;
handlers::update(id, form, session).await
},
(&Method::DELETE, Some(id)) => {
let id: i64 = id.parse().map_err(|_| ServerError::NotFound)?;
handlers::delete(id, session).await
@ -43,6 +48,18 @@ mod handlers {
json_status_response(cidr, StatusCode::CREATED)
}
pub async fn update(
id: i64,
form: CidrContents,
session: Session,
) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let cidr = DatabaseCidr::get(&conn, id)?;
DatabaseCidr::from(cidr).update(&conn, form)?;
status_response(StatusCode::NO_CONTENT)
}
pub async fn list(session: Session) -> Result<Response<Body>, ServerError> {
let conn = session.context.db.lock();
let cidrs = DatabaseCidr::list(&conn)?;

View File

@ -2,7 +2,7 @@ use crate::ServerError;
use ipnet::IpNet;
use rusqlite::{params, Connection};
use shared::{Cidr, CidrContents};
use std::ops::Deref;
use std::ops::{Deref, DerefMut};
pub static CREATE_TABLE_SQL: &str = "CREATE TABLE cidrs (
id INTEGER PRIMARY KEY,
@ -35,6 +35,12 @@ impl Deref for DatabaseCidr {
}
}
impl DerefMut for DatabaseCidr {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl DatabaseCidr {
pub fn create(conn: &Connection, contents: CidrContents) -> Result<Cidr, ServerError> {
let CidrContents { name, cidr, parent } = &contents;
@ -106,6 +112,23 @@ impl DatabaseCidr {
Ok(Cidr { id, contents })
}
/// Update self with new contents, validating them and updating the backend in the process.
/// Currently this only supports updating the name and ignores changes to any other field.
pub fn update(&mut self, conn: &Connection, contents: CidrContents) -> Result<(), ServerError> {
let new_contents = CidrContents {
name: contents.name,
..self.contents.clone()
};
conn.execute(
"UPDATE cidrs SET name = ?2 WHERE id = ?1",
params![self.id, &*new_contents.name,],
)?;
self.contents = new_contents;
Ok(())
}
pub fn delete(conn: &Connection, id: i64) -> Result<(), ServerError> {
conn.execute("DELETE FROM cidrs WHERE id = ?1", params![id])?;
Ok(())

View File

@ -10,7 +10,8 @@ use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use shared::{
get_local_addrs, AddCidrOpts, AddPeerOpts, DeleteCidrOpts, EnableDisablePeerOpts, Endpoint,
IoErrorContext, NetworkOpts, PeerContents, RenamePeerOpts, INNERNET_PUBKEY_HEADER,
IoErrorContext, NetworkOpts, PeerContents, RenameCidrOpts, RenamePeerOpts,
INNERNET_PUBKEY_HEADER,
};
use std::{
collections::{HashMap, VecDeque},
@ -126,6 +127,14 @@ enum Command {
args: AddCidrOpts,
},
/// Rename an existing CIDR.
RenameCidr {
interface: Interface,
#[clap(flatten)]
args: RenameCidrOpts,
},
/// Delete a CIDR.
DeleteCidr {
interface: Interface,
@ -288,6 +297,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
enable_or_disable_peer(&interface, &conf, true, opts.network, args)?
},
Command::AddCidr { interface, args } => add_cidr(&interface, &conf, args)?,
Command::RenameCidr { interface, args } => rename_cidr(&interface, &conf, args)?,
Command::DeleteCidr { interface, args } => delete_cidr(&interface, &conf, args)?,
Command::Completions { shell } => {
use clap::CommandFactory;
@ -460,6 +470,27 @@ fn add_cidr(
Ok(())
}
fn rename_cidr(
interface: &InterfaceName,
conf: &ServerConfig,
opts: RenameCidrOpts,
) -> Result<(), Error> {
let conn = open_database_connection(interface, conf)?;
let cidrs = DatabaseCidr::list(&conn)?;
if let Some((cidr_request, old_name)) = shared::prompts::rename_cidr(&cidrs, &opts)? {
let db_cidr = DatabaseCidr::list(&conn)?
.into_iter()
.find(|c| c.name == old_name)
.ok_or_else(|| anyhow!("CIDR not found."))?;
db::DatabaseCidr::from(db_cidr).update(&conn, cidr_request)?;
} else {
println!("exited without renaming CIDR.");
}
Ok(())
}
fn delete_cidr(
interface: &InterfaceName,
conf: &ServerConfig,

View File

@ -2,7 +2,8 @@ use crate::{
interface_config::{InterfaceConfig, InterfaceInfo, ServerInfo},
AddCidrOpts, AddDeleteAssociationOpts, AddPeerOpts, Association, Cidr, CidrContents, CidrTree,
DeleteCidrOpts, EnableDisablePeerOpts, Endpoint, Error, Hostname, IpNetExt, ListenPortOpts,
OverrideEndpointOpts, Peer, PeerContents, RenamePeerOpts, PERSISTENT_KEEPALIVE_INTERVAL_SECS,
OverrideEndpointOpts, Peer, PeerContents, RenameCidrOpts, RenamePeerOpts,
PERSISTENT_KEEPALIVE_INTERVAL_SECS,
};
use anyhow::anyhow;
use colored::*;
@ -111,6 +112,49 @@ pub fn add_cidr(cidrs: &[Cidr], request: &AddCidrOpts) -> Result<Option<CidrCont
)
}
/// Bring up a prompt to rename an existing CIDR. Returns the CIDR request.
pub fn rename_cidr(
cidrs: &[Cidr],
args: &RenameCidrOpts,
) -> Result<Option<(CidrContents, String)>, Error> {
let old_cidr = if let Some(ref name) = args.name {
cidrs
.iter()
.find(|c| &c.name == name)
.ok_or_else(|| anyhow!("CIDR '{}' does not exist", name))?
.clone()
} else {
let (cidr_index, _) = select(
"CIDR to rename",
&cidrs.iter().map(|ep| ep.name.clone()).collect::<Vec<_>>(),
)?;
cidrs[cidr_index].clone()
};
let old_name = old_cidr.name.clone();
let new_name = if let Some(ref name) = args.new_name {
name.clone()
} else {
input("New Name", Prefill::None)?
};
let mut new_cidr = old_cidr;
new_cidr.contents.name = new_name.clone();
Ok(
if args.yes
|| confirm(&format!(
"Rename CIDR {} to {}?",
old_name.yellow(),
new_name.yellow()
))?
{
Some((new_cidr.contents, old_name))
} else {
None
},
)
}
/// Bring up a prompt to delete a CIDR. Returns the peer request.
pub fn delete_cidr(cidrs: &[Cidr], peers: &[Peer], request: &DeleteCidrOpts) -> Result<i64, Error> {
let eligible_cidrs: Vec<_> = cidrs
@ -342,7 +386,7 @@ pub fn add_peer(
)
}
/// Bring up a prompt to create a new peer. Returns the peer request.
/// Bring up a prompt to rename an existing peer. Returns the peer request.
pub fn rename_peer(
peers: &[Peer],
args: &RenamePeerOpts,

View File

@ -381,6 +381,21 @@ pub struct AddCidrOpts {
pub yes: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct RenameCidrOpts {
/// Name of CIDR to rename
#[clap(long)]
pub name: Option<String>,
/// The new name of the CIDR
#[clap(long)]
pub new_name: Option<String>,
/// Bypass confirmation
#[clap(long)]
pub yes: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub struct DeleteCidrOpts {
/// The CIDR name (eg. 'engineers')