Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rust): update the postgres schema #8746

Merged
merged 3 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 78 additions & 77 deletions implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use tokio::sync::broadcast::{channel, Receiver, Sender};

use ockam::SqlxDatabase;
use ockam_core::env::get_env_with_default;
use ockam_node::database::{DatabaseConfiguration, OCKAM_SQLITE_IN_MEMORY};
use ockam_node::database::{DatabaseConfiguration, DatabaseType, OCKAM_SQLITE_IN_MEMORY};
use ockam_node::Executor;

use crate::cli_state::error::Result;
Expand Down Expand Up @@ -136,11 +136,19 @@ impl CliState {
}

/// Stop nodes and remove all the directories storing state
/// Don't touch the database data if Postgres is used and reset was called accidentally.
pub async fn reset(&self) -> Result<()> {
self.delete_all_named_identities().await?;
self.delete_all_nodes().await?;
self.delete_all_named_vaults().await?;
self.delete().await
if Self::make_database_configuration(&self.mode)?.database_type() == DatabaseType::Postgres
{
Err(CliStateError::InvalidOperation(
"Cannot reset the database when using Postgres".to_string(),
))
} else {
self.delete_all_named_identities().await?;
self.delete_all_nodes().await?;
self.delete_all_named_vaults().await?;
self.delete().await
}
}

/// Removes all the directories storing state without loading the current state
Expand All @@ -152,7 +160,6 @@ impl CliState {

/// Delete the local database and log files
pub async fn delete(&self) -> Result<()> {
self.database.drop_postgres_node_tables().await?;
self.delete_local_data()
}

Expand Down Expand Up @@ -363,82 +370,76 @@ impl CliStateMode {
mod tests {
use super::*;
use itertools::Itertools;
use ockam_node::database::DatabaseType;
use sqlx::any::AnyRow;
use sqlx::Row;
use ockam_node::database::{skip_if_postgres, DatabaseType};
use std::fs;
use tempfile::NamedTempFile;

#[tokio::test]
async fn test_reset() -> Result<()> {
let db_file = NamedTempFile::new().unwrap();
let cli_state_directory = db_file.path().parent().unwrap().join(random_name());
let mode = CliStateMode::Persistent(cli_state_directory.clone());
let db = SqlxDatabase::create(&CliState::make_database_configuration(&mode)?).await?;
db.drop_all_postgres_tables().await?;
let cli = CliState::create(mode).await?;

// create 2 vaults
// the second vault is using a separate file
let _vault1 = cli.get_or_create_named_vault("vault1").await?;
let _vault2 = cli.get_or_create_named_vault("vault2").await?;

// create 2 identities
let identity1 = cli
.create_identity_with_name_and_vault("identity1", "vault1")
.await?;
let identity2 = cli
.create_identity_with_name_and_vault("identity2", "vault2")
.await?;

// create 2 nodes
let _node1 = cli
.create_node_with_identifier("node1", &identity1.identifier())
.await?;
let _node2 = cli
.create_node_with_identifier("node2", &identity2.identifier())
.await?;

let file_names = list_file_names(&cli_state_directory);
let expected = match cli.database_configuration()?.database_type() {
DatabaseType::Sqlite => vec![
"vault-vault2".to_string(),
"application_database.sqlite3".to_string(),
"database.sqlite3".to_string(),
],
DatabaseType::Postgres => vec!["vault-vault2".to_string()],
};

assert_eq!(
file_names.iter().sorted().as_slice(),
expected.iter().sorted().as_slice()
);

// reset the local state
cli.reset().await?;
let result = fs::read_dir(&cli_state_directory);
assert!(result.is_ok(), "the cli state directory is not deleted");

match cli.database_configuration()?.database_type() {
DatabaseType::Sqlite => {
// When the database is SQLite, only the application database must remain
let file_names = list_file_names(&cli_state_directory);
let expected = vec!["application_database.sqlite3".to_string()];
assert_eq!(file_names, expected);
}
DatabaseType::Postgres => {
// When the database is Postgres, only the journey tables must remain
let tables: Vec<AnyRow> = sqlx::query(
"SELECT tablename::text FROM pg_tables WHERE schemaname = 'public'",
)
.fetch_all(&*db.pool)
.await
.unwrap();
let actual: Vec<String> = tables.iter().map(|r| r.get(0)).sorted().collect();
assert_eq!(actual, vec!["host_journey", "project_journey"]);
}
};
Ok(())
// We don't need to test reset with Postgres since we don't want reset be used accidentally
// with a Postgres database (resetting in that case throws an error).
skip_if_postgres(|| async {
let db_file = NamedTempFile::new().unwrap();
let cli_state_directory = db_file.path().parent().unwrap().join(random_name());
let mode = CliStateMode::Persistent(cli_state_directory.clone());
let cli = CliState::create(mode).await?;

// create 2 vaults
// the second vault is using a separate file
let _vault1 = cli.get_or_create_named_vault("vault1").await?;
let _vault2 = cli.get_or_create_named_vault("vault2").await?;

// create 2 identities
let identity1 = cli
.create_identity_with_name_and_vault("identity1", "vault1")
.await?;
let identity2 = cli
.create_identity_with_name_and_vault("identity2", "vault2")
.await?;

// create 2 nodes
let _node1 = cli
.create_node_with_identifier("node1", &identity1.identifier())
.await?;
let _node2 = cli
.create_node_with_identifier("node2", &identity2.identifier())
.await?;

let file_names = list_file_names(&cli_state_directory);

// this test is not executed with Postgres
let expected = match cli.database_configuration()?.database_type() {
DatabaseType::Sqlite => vec![
"vault-vault2".to_string(),
"application_database.sqlite3".to_string(),
"database.sqlite3".to_string(),
],
DatabaseType::Postgres => vec![],
};

assert_eq!(
file_names.iter().sorted().as_slice(),
expected.iter().sorted().as_slice()
);

// reset the local state
cli.reset().await?;
let result = fs::read_dir(&cli_state_directory);
assert!(result.is_ok(), "the cli state directory is not deleted");

// this test is not executed with Postgres
match cli.database_configuration()?.database_type() {
DatabaseType::Sqlite => {
// When the database is SQLite, only the application database must remain
let file_names = list_file_names(&cli_state_directory);
let expected = vec!["application_database.sqlite3".to_string()];
assert_eq!(file_names, expected);
}
DatabaseType::Postgres => (),
};
Ok(())
})
.await
}

/// HELPERS
Expand Down
97 changes: 8 additions & 89 deletions implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ use sysinfo::{Pid, ProcessStatus, ProcessesToUpdate, System};

use crate::cli_state::{random_name, NamedVault, Result};
use crate::cli_state::{CliState, CliStateError};
use crate::cloud::project::Project;
use crate::colors::color_primary;
use crate::config::lookup::InternetAddress;

Expand All @@ -27,17 +26,15 @@ use crate::{fmt_warn, ConnectionStatus};
/// The methods below support the creation and update of local nodes
impl CliState {
/// Create a node, with some optional associated values, and start it
#[instrument(skip_all, fields(node_name = node_name, identity_name = identity_name.clone(), project_name = project_name.clone()
))]
#[instrument(skip_all, fields(node_name = node_name, identity_name = identity_name.clone()))]
pub async fn start_node_with_optional_values(
&self,
node_name: &str,
identity_name: &Option<String>,
project_name: &Option<String>,
tcp_listener: Option<&TcpListener>,
) -> Result<NodeInfo> {
let mut node = self
.create_node_with_optional_values(node_name, identity_name, project_name)
.create_node_with_optional_identity(node_name, identity_name)
.await?;
if node.pid.is_none() {
let pid = process::id();
Expand All @@ -53,17 +50,12 @@ impl CliState {
Ok(node)
}

/// Create a node, with some optional associated values:
///
/// - an identity name. That identity is used by the `NodeManager` to create secure channels
/// - a project name. It is used to create policies on resources provisioned on a node (like a TCP outlet for example)
#[instrument(skip_all, fields(node_name = node_name, identity_name = identity_name.clone(), project_name = project_name.clone()
))]
pub async fn create_node_with_optional_values(
/// Create a node, with an optional identity name. That identity is used by the `NodeManager` to create secure channels
#[instrument(skip_all, fields(node_name = node_name, identity_name = identity_name.clone()))]
pub async fn create_node_with_optional_identity(
&self,
node_name: &str,
identity_name: &Option<String>,
project_name: &Option<String>,
) -> Result<NodeInfo> {
let identity = match identity_name {
Some(name) => self.get_named_identity(name).await?,
Expand All @@ -72,7 +64,6 @@ impl CliState {
let node = self
.create_node_with_identifier(node_name, &identity.identifier())
.await?;
self.set_node_project(node_name, project_name).await?;
Ok(node)
}

Expand Down Expand Up @@ -134,27 +125,6 @@ impl CliState {
Ok(())
}

/// This method can be used to start a local node first
/// then create a project, and associate it to the node
#[instrument(skip_all, fields(node_name = node_name, project_name = project_name.clone()))]
pub async fn set_node_project(
&self,
node_name: &str,
project_name: &Option<String>,
) -> Result<()> {
let project = match project_name {
Some(name) => Some(self.projects().get_project_by_name(name).await?),
None => self.projects().get_default_project().await.ok(),
};

if let Some(project) = project {
self.nodes_repository()
.set_node_project_name(node_name, project.name())
.await?
};
Ok(())
}

/// Remove a node:
///
/// - remove it from the repository
Expand Down Expand Up @@ -381,23 +351,6 @@ impl CliState {
}
}

/// Return the project associated to a node if there is one
#[instrument(skip_all, fields(node_name = node_name))]
pub async fn get_node_project(&self, node_name: &str) -> Result<Project> {
match self
.nodes_repository()
.get_node_project_name(node_name)
.await?
{
Some(project_name) => self.projects().get_project_by_name(&project_name).await,
None => Err(Error::new(
Origin::Api,
Kind::NotFound,
format!("there is no project associated to node {node_name}"),
))?,
}
}

/// Return the stdout log file used by a node
#[instrument(skip_all, fields(node_name = node_name))]
pub fn stdout_logs(&self, node_name: &str) -> Result<PathBuf> {
Expand Down Expand Up @@ -701,7 +654,6 @@ impl NodeInfo {
#[cfg(test)]
mod tests {
use super::*;
use crate::cloud::project::models::ProjectModel;
use crate::config::lookup::InternetAddress;
use std::net::SocketAddr;
use std::str::FromStr;
Expand Down Expand Up @@ -799,57 +751,24 @@ mod tests {
}

#[tokio::test]
async fn test_create_node_with_optional_values() -> Result<()> {
async fn test_create_node_with_optional_identity() -> Result<()> {
let cli = CliState::test().await?;

// a node can be created with just a name
let node = cli
.create_node_with_optional_values("node-1", &None, &None)
.create_node_with_optional_identity("node-1", &None)
.await?;
let result = cli.get_node(&node.name()).await?;
assert_eq!(result.name(), node.name());

// a node can be created with a name and an existing identity
let identity = cli.create_identity_with_name("name").await?;
let node = cli
.create_node_with_optional_values("node-2", &Some(identity.name()), &None)
.create_node_with_optional_identity("node-2", &Some(identity.name()))
.await?;
let result = cli.get_node(&node.name()).await?;
assert_eq!(result.identifier(), identity.identifier());

// a node can be created with a name, an existing identity and an existing project
let project = ProjectModel {
id: "project_id".to_string(),
name: "project_name".to_string(),
space_name: "1".to_string(),
access_route: "".to_string(),
users: vec![],
space_id: "1".to_string(),
identity: None,
project_change_history: None,
authority_access_route: None,
authority_identity: None,
okta_config: None,
kafka_config: None,
version: None,
running: None,
operation_id: None,
user_roles: vec![],
};
cli.projects()
.import_and_store_project(project.clone())
.await?;

let node = cli
.create_node_with_optional_values(
"node-4",
&Some(identity.name()),
&Some(project.name.clone()),
)
.await?;
let result = cli.get_node_project(&node.name()).await?;
assert_eq!(result.name(), &project.name);

Ok(())
}
}
Loading
Loading