diff --git a/data_types/src/deleted_database.rs b/data_types/src/detailed_database.rs similarity index 70% rename from data_types/src/deleted_database.rs rename to data_types/src/detailed_database.rs index 1c0d1f4574..be1a7d62dc 100644 --- a/data_types/src/deleted_database.rs +++ b/data_types/src/detailed_database.rs @@ -2,12 +2,15 @@ use crate::DatabaseName; use chrono::{DateTime, Utc}; use std::{fmt, str::FromStr}; -/// Metadata about a deleted database that could be restored or permanently deleted. +/// Detailed metadata about a database. #[derive(Debug, Clone, PartialEq)] -pub struct DeletedDatabase { +pub struct DetailedDatabase { + /// The name of the database pub name: DatabaseName<'static>, + /// The generation ID of the database in object storage pub generation_id: GenerationId, - pub deleted_at: DateTime<Utc>, + /// The UTC datetime at which this database was deleted, if applicable + pub deleted_at: Option<DateTime<Utc>>, } /// Identifier for a generation of a particular database diff --git a/data_types/src/lib.rs b/data_types/src/lib.rs index 062af41bb8..f5858c9d56 100644 --- a/data_types/src/lib.rs +++ b/data_types/src/lib.rs @@ -14,7 +14,7 @@ pub mod chunk_metadata; pub mod consistent_hasher; mod database_name; pub mod database_rules; -pub mod deleted_database; +pub mod detailed_database; pub mod error; pub mod instant; pub mod job; diff --git a/generated_types/protos/influxdata/iox/management/v1/service.proto b/generated_types/protos/influxdata/iox/management/v1/service.proto index a6715fcbf2..c19b54aea0 100644 --- a/generated_types/protos/influxdata/iox/management/v1/service.proto +++ b/generated_types/protos/influxdata/iox/management/v1/service.proto @@ -42,6 +42,9 @@ service ManagementService { // List deleted databases and their metadata. rpc ListDeletedDatabases(ListDeletedDatabasesRequest) returns (ListDeletedDatabasesResponse); + // List all databases and their metadata. + rpc ListDetailedDatabases(ListDetailedDatabasesRequest) returns (ListDetailedDatabasesResponse); + // List chunks available on this database rpc ListChunks(ListChunksRequest) returns (ListChunksResponse); @@ -189,18 +192,24 @@ message RestoreDatabaseResponse {} message ListDeletedDatabasesRequest {} message ListDeletedDatabasesResponse { - repeated DeletedDatabase deleted_databases = 1; + repeated DetailedDatabase deleted_databases = 1; } -// This resource represents a deleted database. -message DeletedDatabase { - // The generation ID of the deleted database. +message ListDetailedDatabasesRequest {} + +message ListDetailedDatabasesResponse { + repeated DetailedDatabase databases = 1; +} + +// This resource represents detailed information about a database. +message DetailedDatabase { + // The generation ID of the database. uint64 generation_id = 1; - // The UTC datetime at which this database was deleted. + // The UTC datetime at which this database was deleted, if applicable. google.protobuf.Timestamp deleted_at = 2; - // The name of the deleted database. + // The name of the database. string db_name = 3; } diff --git a/generated_types/src/deleted_database.rs b/generated_types/src/deleted_database.rs deleted file mode 100644 index 7b485cb6e7..0000000000 --- a/generated_types/src/deleted_database.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::influxdata::iox::management::v1 as management; -use data_types::deleted_database::DeletedDatabase; - -impl From<DeletedDatabase> for management::DeletedDatabase { - fn from(deleted: DeletedDatabase) -> Self { - let DeletedDatabase { - name, - generation_id, - deleted_at, - } = deleted; - - Self { - db_name: name.to_string(), - generation_id: generation_id.inner as u64, - deleted_at: Some(deleted_at.into()), - } - } -} diff --git a/generated_types/src/detailed_database.rs b/generated_types/src/detailed_database.rs new file mode 100644 index 0000000000..44b7d1cb1a --- /dev/null +++ b/generated_types/src/detailed_database.rs @@ -0,0 +1,18 @@ +use crate::influxdata::iox::management::v1 as management; +use data_types::detailed_database::DetailedDatabase; + +impl From<DetailedDatabase> for management::DetailedDatabase { + fn from(database: DetailedDatabase) -> Self { + let DetailedDatabase { + name, + generation_id, + deleted_at, + } = database; + + Self { + db_name: name.to_string(), + generation_id: generation_id.inner as u64, + deleted_at: deleted_at.map(Into::into), + } + } +} diff --git a/generated_types/src/lib.rs b/generated_types/src/lib.rs index 0176d23b84..84b38f56e9 100644 --- a/generated_types/src/lib.rs +++ b/generated_types/src/lib.rs @@ -143,7 +143,7 @@ pub use influxdata::platform::storage::*; pub mod chunk; pub mod database_rules; pub mod database_state; -pub mod deleted_database; +pub mod detailed_database; pub mod google; pub mod job; diff --git a/influxdb_iox_client/src/client/management.rs b/influxdb_iox_client/src/client/management.rs index 8a1ea54f9c..b47e9e2c37 100644 --- a/influxdb_iox_client/src/client/management.rs +++ b/influxdb_iox_client/src/client/management.rs @@ -604,7 +604,7 @@ impl Client { /// List deleted databases and metadata pub async fn list_deleted_databases( &mut self, - ) -> Result<Vec<DeletedDatabase>, ListDatabaseError> { + ) -> Result<Vec<DetailedDatabase>, ListDatabaseError> { let response = self .inner .list_deleted_databases(ListDeletedDatabasesRequest {}) @@ -616,6 +616,21 @@ impl Client { Ok(response.into_inner().deleted_databases) } + /// List all databases and detailed metadata + pub async fn list_detailed_databases( + &mut self, + ) -> Result<Vec<DetailedDatabase>, ListDatabaseError> { + let response = self + .inner + .list_detailed_databases(ListDetailedDatabasesRequest {}) + .await + .map_err(|status| match status.code() { + tonic::Code::Unavailable => ListDatabaseError::Unavailable(status), + _ => ListDatabaseError::ServerError(status), + })?; + Ok(response.into_inner().databases) + } + /// Get database configuration /// /// If `omit_defaults` is false, return the current configuration diff --git a/iox_object_store/src/lib.rs b/iox_object_store/src/lib.rs index 28798ab1d6..a0b7d5d071 100644 --- a/iox_object_store/src/lib.rs +++ b/iox_object_store/src/lib.rs @@ -17,7 +17,7 @@ use bytes::{Bytes, BytesMut}; use chrono::{DateTime, Utc}; use data_types::{ - deleted_database::{DeletedDatabase, GenerationId}, + detailed_database::{DetailedDatabase, GenerationId}, error::ErrorLogger, server_id::ServerId, DatabaseName, @@ -96,19 +96,19 @@ pub struct IoxObjectStore { #[derive(Debug, Copy, Clone, PartialEq)] struct Generation { id: GenerationId, - deleted: Option<DateTime<Utc>>, + deleted_at: Option<DateTime<Utc>>, } impl Generation { fn active(id: usize) -> Self { Self { id: GenerationId { inner: id }, - deleted: None, + deleted_at: None, } } fn is_active(&self) -> bool { - self.deleted.is_none() + self.deleted_at.is_none() } } @@ -147,28 +147,46 @@ impl IoxObjectStore { pub async fn list_deleted_databases( inner: &ObjectStore, server_id: ServerId, - ) -> Result<Vec<DeletedDatabase>> { - let mut deleted_databases = vec![]; + ) -> Result<Vec<DetailedDatabase>> { + Ok(Self::list_all_databases(inner, server_id) + .await? + .into_iter() + .flat_map(|(name, generations)| { + let name = Arc::new(name); + generations.into_iter().filter_map(move |gen| { + let name = Arc::clone(&name); + gen.deleted_at.map(|_| DetailedDatabase { + name: (*name).clone(), + generation_id: gen.id, + deleted_at: gen.deleted_at, + }) + }) + }) + .collect()) + } - let all_dbs = Self::list_all_databases(inner, server_id).await; - - for (name, generations) in all_dbs? { - for deleted_gen in generations { - if let Generation { - id, - deleted: Some(deleted_at), - } = deleted_gen - { - deleted_databases.push(DeletedDatabase { - name: name.clone(), - generation_id: id, - deleted_at, - }); - } - } - } - - Ok(deleted_databases) + /// List all databases in in object storage along with their generation IDs and if/when they + /// were deleted. Useful for visibility into object storage and finding databases to restore or + /// permanently delete. + pub async fn list_detailed_databases( + inner: &ObjectStore, + server_id: ServerId, + ) -> Result<Vec<DetailedDatabase>> { + Ok(Self::list_all_databases(inner, server_id) + .await? + .into_iter() + .flat_map(|(name, generations)| { + let name = Arc::new(name); + generations.into_iter().map(move |gen| { + let name = Arc::clone(&name); + DetailedDatabase { + name: (*name).clone(), + generation_id: gen.id, + deleted_at: gen.deleted_at, + } + }) + }) + .collect()) } /// List database names in object storage along with all existing generations for each database @@ -228,13 +246,13 @@ impl IoxObjectStore { let generation_list_result = inner.list_with_delimiter(&prefix).await?; let tombstone_file = TombstonePath::new_from_object_store_path(&prefix); - let deleted = generation_list_result + let deleted_at = generation_list_result .objects .into_iter() .find(|object| object.location == tombstone_file.inner) .map(|object| object.last_modified); - generations.push(Generation { id, deleted }); + generations.push(Generation { id, deleted_at }); } else { // Deliberately ignoring errors with parsing; if the directory isn't a usize, it's // not a valid database generation directory and we should skip it. @@ -965,7 +983,7 @@ mod tests { generations[0], Generation { id: GenerationId { inner: 0 }, - deleted: None, + deleted_at: None, } ); @@ -999,7 +1017,7 @@ mod tests { generations[1], Generation { id: GenerationId { inner: 1 }, - deleted: None, + deleted_at: None, } ); } diff --git a/server/src/database.rs b/server/src/database.rs index 1cbbe5eee6..9dc1891a25 100644 --- a/server/src/database.rs +++ b/server/src/database.rs @@ -9,7 +9,7 @@ use crate::{ }; use chrono::{DateTime, Utc}; use data_types::{ - database_rules::WriteBufferDirection, deleted_database::GenerationId, server_id::ServerId, + database_rules::WriteBufferDirection, detailed_database::GenerationId, server_id::ServerId, DatabaseName, }; use futures::{ diff --git a/server/src/lib.rs b/server/src/lib.rs index 804aee59d2..733539290f 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -72,7 +72,7 @@ use async_trait::async_trait; use chrono::Utc; use data_types::{ database_rules::{NodeGroup, RoutingRules, ShardId, Sink}, - deleted_database::DeletedDatabase, + detailed_database::DetailedDatabase, error::ErrorLogger, job::Job, server_id::ServerId, @@ -241,6 +241,9 @@ pub enum Error { #[snafu(display("error listing deleted databases in object storage: {}", source))] ListDeletedDatabases { source: object_store::Error }, + + #[snafu(display("error listing detailed databases in object storage: {}", source))] + ListDetailedDatabases { source: object_store::Error }, } pub type Result<T, E = Error> = std::result::Result<T, E>; @@ -725,8 +728,8 @@ where Ok(()) } - /// List all deleted databases in object storage. - pub async fn list_deleted_databases(&self) -> Result<Vec<DeletedDatabase>> { + /// List deleted databases in object storage. + pub async fn list_deleted_databases(&self) -> Result<Vec<DetailedDatabase>> { let server_id = { let state = self.shared.state.read(); let initialized = state.initialized()?; @@ -741,6 +744,22 @@ where .context(ListDeletedDatabases)?) } + /// List all databases, active and deleted, in object storage, including their generation IDs. + pub async fn list_detailed_databases(&self) -> Result<Vec<DetailedDatabase>> { + let server_id = { + let state = self.shared.state.read(); + let initialized = state.initialized()?; + initialized.server_id + }; + + Ok(IoxObjectStore::list_detailed_databases( + self.shared.application.object_store(), + server_id, + ) + .await + .context(ListDetailedDatabases)?) + } + pub async fn write_pb(&self, database_batch: pb::DatabaseBatch) -> Result<()> { let db_name = DatabaseName::new(database_batch.database_name.as_str()) .context(InvalidDatabaseName)?; diff --git a/src/commands/database.rs b/src/commands/database.rs index d0dd7678c5..36d961585f 100644 --- a/src/commands/database.rs +++ b/src/commands/database.rs @@ -11,6 +11,7 @@ use influxdb_iox_client::{ }, write::{self, WriteError}, }; +use prettytable::{format, Cell, Row, Table}; use std::{ convert::TryInto, fs::File, io::Read, num::NonZeroU64, path::PathBuf, str::FromStr, time::Duration, @@ -141,6 +142,11 @@ struct List { /// Whether to list databases marked as deleted instead, to restore or permanently delete. #[structopt(long)] deleted: bool, + + /// Whether to list detailed information, including generation IDs, about all databases, + /// whether they are active or marked as deleted. + #[structopt(long)] + detailed: bool, } /// Return configuration of specific database @@ -258,23 +264,37 @@ pub async fn command(connection: Connection, config: Config) -> Result<()> { } Command::List(list) => { let mut client = management::Client::new(connection); - if list.deleted { - let deleted = client.list_deleted_databases().await?; - println!("Deleted at | Generation ID | Name"); - println!("--------------------------------+---------------+--------"); - for database in deleted { + if list.deleted || list.detailed { + let databases = if list.deleted { + client.list_deleted_databases().await? + } else { + client.list_detailed_databases().await? + }; + + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(Row::new(vec![ + Cell::new("Deleted at"), + Cell::new("Generation ID"), + Cell::new("Name"), + ])); + + for database in databases { let deleted_at = database .deleted_at .and_then(|t| { let dt: Result<DateTime<Utc>, _> = t.try_into(); dt.ok().map(|d| d.to_string()) }) - .unwrap_or_else(|| String::from("Unknown")); - println!( - "{:<33}{:<16}{}", - deleted_at, database.generation_id, database.db_name, - ); + .unwrap_or_else(String::new); + table.add_row(Row::new(vec![ + Cell::new(&deleted_at), + Cell::new(&database.generation_id.to_string()), + Cell::new(&database.db_name), + ])); } + + print!("{}", table); } else { let names = client.list_database_names().await?; println!("{}", names.join("\n")) diff --git a/src/influxdb_ioxd/rpc/management.rs b/src/influxdb_ioxd/rpc/management.rs index e9ac840c8d..5df9be086d 100644 --- a/src/influxdb_ioxd/rpc/management.rs +++ b/src/influxdb_ioxd/rpc/management.rs @@ -213,6 +213,22 @@ where })) } + async fn list_detailed_databases( + &self, + _: Request<ListDetailedDatabasesRequest>, + ) -> Result<Response<ListDetailedDatabasesResponse>, Status> { + let databases = self + .server + .list_detailed_databases() + .await + .map_err(default_server_error_handler)? + .into_iter() + .map(Into::into) + .collect(); + + Ok(Response::new(ListDetailedDatabasesResponse { databases })) + } + async fn list_chunks( &self, request: Request<ListChunksRequest>, diff --git a/tests/end_to_end_cases/management_cli.rs b/tests/end_to_end_cases/management_cli.rs index 8236398a34..72671eefe2 100644 --- a/tests/end_to_end_cases/management_cli.rs +++ b/tests/end_to_end_cases/management_cli.rs @@ -183,6 +183,24 @@ async fn test_create_database_immutable() { .stdout(predicate::str::contains(r#""immutable": true"#)); } +const DELETED_DB_DATETIME: &str = r#"[\d-]+\s[\d:\.]+\s[A-Z]+"#; + +fn deleted_db_match(db: &str, generation_id: usize) -> predicates::str::RegexPredicate { + predicate::str::is_match(format!( + r#"(?m)^\|\s+{}\s+\|\s+{}\s+\|\s+{}\s+\|$"#, + DELETED_DB_DATETIME, generation_id, db + )) + .unwrap() +} + +fn active_db_match(db: &str, generation_id: usize) -> predicates::str::RegexPredicate { + predicate::str::is_match(format!( + r#"(?m)^\|\s+\|\s+{}\s+\|\s+{}\s+\|$"#, + generation_id, db + )) + .unwrap() +} + #[tokio::test] async fn delete_database() { let server_fixture = ServerFixture::create_shared().await; @@ -224,6 +242,18 @@ async fn delete_database() { .success() .stdout(predicate::str::contains(db).not()); + // Listing detailed database info does include the active database, along with its generation + Command::cargo_bin("influxdb_iox") + .unwrap() + .arg("database") + .arg("list") + .arg("--detailed") + .arg("--host") + .arg(addr) + .assert() + .success() + .stdout(active_db_match(db, 0)); + // Delete the database Command::cargo_bin("influxdb_iox") .unwrap() @@ -257,7 +287,19 @@ async fn delete_database() { .arg(addr) .assert() .success() - .stdout(predicate::str::contains(db)); + .stdout(deleted_db_match(db, 0)); + + // Listing detailed database info does include the deleted database + Command::cargo_bin("influxdb_iox") + .unwrap() + .arg("database") + .arg("list") + .arg("--detailed") + .arg("--host") + .arg(addr) + .assert() + .success() + .stdout(deleted_db_match(db, 0)); // Deleting the database again is an error Command::cargo_bin("influxdb_iox") @@ -306,7 +348,19 @@ async fn delete_database() { .arg(addr) .assert() .success() - .stdout(predicate::str::contains(db)); + .stdout(deleted_db_match(db, 0)); + + // Listing detailed database info includes both active and deleted + Command::cargo_bin("influxdb_iox") + .unwrap() + .arg("database") + .arg("list") + .arg("--detailed") + .arg("--host") + .arg(addr) + .assert() + .success() + .stdout(deleted_db_match(db, 0).and(active_db_match(db, 1))); // Delete the 2nd database Command::cargo_bin("influxdb_iox") @@ -341,10 +395,19 @@ async fn delete_database() { .arg(addr) .assert() .success() - .stdout( - predicate::str::contains(format!("0 {}", db)) - .and(predicate::str::contains(format!("1 {}", db))), - ); + .stdout(deleted_db_match(db, 0).and(deleted_db_match(db, 1))); + + // Listing detailed database info includes both deleted generations + Command::cargo_bin("influxdb_iox") + .unwrap() + .arg("database") + .arg("list") + .arg("--detailed") + .arg("--host") + .arg(addr) + .assert() + .success() + .stdout(deleted_db_match(db, 0).and(deleted_db_match(db, 1))); // Restore generation 0 Command::cargo_bin("influxdb_iox") @@ -383,10 +446,19 @@ async fn delete_database() { .arg(addr) .assert() .success() - .stdout( - predicate::str::contains(format!("1 {}", db)) - .and(predicate::str::contains(format!("0 {}", db)).not()), - ); + .stdout(deleted_db_match(db, 0).not().and(deleted_db_match(db, 1))); + + // Listing detailed database info includes both active and deleted + Command::cargo_bin("influxdb_iox") + .unwrap() + .arg("database") + .arg("list") + .arg("--detailed") + .arg("--host") + .arg(addr) + .assert() + .success() + .stdout(active_db_match(db, 0).and(deleted_db_match(db, 1))); } #[tokio::test]