Refacto databases (#676)

Refacto databases
This commit is contained in:
Erèbe - Romain Gerard
2022-03-30 23:01:34 +02:00
committed by GitHub
parent 5bb6e4a37d
commit 2a6760ff87
42 changed files with 2275 additions and 5696 deletions

View File

@@ -1,5 +0,0 @@
pub mod mongodb;
pub mod mysql;
pub mod postgresql;
pub mod redis;
pub mod utilities;

View File

@@ -1,451 +0,0 @@
use std::collections::HashMap;
use crate::cloud_provider::aws::databases::utilities::aws_final_snapshot_name;
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{
generate_supported_version, get_self_hosted_mongodb_version, get_supported_version_to_use, print_action,
};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::{CommandError, EngineError};
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct MongoDbAws {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl MongoDbAws {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
MongoDbAws {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(
&self,
is_managed_services: bool,
event_details: EventDetails,
) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_mongodb_version(self.version(), is_managed_services),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"aws"
}
fn struct_name(&self) -> &str {
"mongodb"
}
}
impl StatefulService for MongoDbAws {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl Service for MongoDbAws {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::MongoDB(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
// https://docs.aws.amazon.com/documentdb/latest/developerguide/limits.html#limits-naming_constraints
let prefix = "mongodb";
let max_size = 60 - prefix.len(); // 63 (max DocumentDB) - 3 (k8s statefulset chars)
let mut new_name = format!("{}{}", prefix, self.name().replace('_', "").replace('-', ""));
if new_name.chars().count() > max_size {
new_name = new_name[..max_size].to_string();
}
new_name
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Default
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, target.kubernetes, target.environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = self
.matching_correct_version(self.is_managed_service(), event_details)?
.matched_version()
.to_string();
context.insert("version", &version);
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_db_name", self.name.as_str());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("encrypt_disk", &self.options.encrypt_disk);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
context.insert("skip_final_snapshot", &false);
context.insert("final_snapshot_name", &aws_final_snapshot_name(self.id()));
context.insert("delete_automated_backups", &self.context().is_test_cluster());
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for MongoDbAws {}
impl ToTransmitter for MongoDbAws {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Helm for MongoDbAws {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("mongodb-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/mongodb", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/aws/chart_values/mongodb", self.context.lib_root_dir()) // FIXME replace `chart_values` by `charts_values`
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for MongoDbAws {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/aws/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/aws/services/mongodb", self.context.lib_root_dir())
}
}
impl Create for MongoDbAws {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), &*self.logger)
})
}
fn on_create_check(&self) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
self.check_domains(self.listeners.clone(), vec![self.fqdn.as_str()], event_details, self.logger())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for MongoDbAws {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for MongoDbAws {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for MongoDbAws {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}
fn get_mongodb_version(requested_version: String, is_managed_service: bool) -> Result<String, CommandError> {
if is_managed_service {
get_managed_mongodb_version(requested_version)
} else {
get_self_hosted_mongodb_version(requested_version)
}
}
fn get_managed_mongodb_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_mongodb_versions = HashMap::new();
// v3.6.0
let mongo_version = generate_supported_version(3, 6, 6, Some(0), Some(0), None);
supported_mongodb_versions.extend(mongo_version);
// v4.0.0
let mongo_version = generate_supported_version(4, 0, 0, Some(0), Some(0), None);
supported_mongodb_versions.extend(mongo_version);
get_supported_version_to_use("DocumentDB", supported_mongodb_versions, requested_version)
}
#[cfg(test)]
mod tests_mongodb {
use crate::cloud_provider::aws::databases::mongodb::get_mongodb_version;
use crate::errors::ErrorMessageVerbosity;
#[test]
fn check_mongodb_version() {
// managed version
assert_eq!(get_mongodb_version("4".to_string(), true).unwrap(), "4.0.0");
assert_eq!(get_mongodb_version("4.0".to_string(), true).unwrap(), "4.0.0");
assert!(get_mongodb_version("4.4".to_string(), true)
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("DocumentDB 4.4 version is not supported"));
// self-hosted version
assert_eq!(get_mongodb_version("4".to_string(), false).unwrap(), "4.4.4");
assert_eq!(get_mongodb_version("4.2".to_string(), false).unwrap(), "4.2.12");
assert!(get_mongodb_version("3.4".to_string(), false)
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("MongoDB 3.4 version is not supported"));
}
}

View File

@@ -1,473 +0,0 @@
use std::collections::HashMap;
use tera::Context as TeraContext;
use crate::cloud_provider::aws::databases::utilities::{aws_final_snapshot_name, get_parameter_group_from_version};
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{
generate_supported_version, get_self_hosted_mysql_version, get_supported_version_to_use, managed_db_name_sanitizer,
print_action,
};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::{CommandError, EngineError};
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, DatabaseKind, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct MySQLAws {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl MySQLAws {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
Self {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(
&self,
is_managed_services: bool,
event_details: EventDetails,
) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_mysql_version(self.version(), is_managed_services),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"aws"
}
fn struct_name(&self) -> &str {
"mysql"
}
}
impl StatefulService for MySQLAws {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for MySQLAws {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for MySQLAws {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::MySQL(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints
let prefix = "mysql";
let max_size = 63 - 3; // max RDS - k8s statefulset chars
managed_db_name_sanitizer(max_size, prefix, self.name())
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Default
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = &self.matching_correct_version(self.is_managed_service(), event_details.clone())?;
context.insert("version", &version.matched_version().to_string());
if self.is_managed_service() {
let parameter_group_family =
match get_parameter_group_from_version(version.matched_version(), DatabaseKind::Mysql) {
Ok(v) => v,
Err(e) => {
return Err(EngineError::new_terraform_unsupported_context_parameter_value(
event_details,
"MySQL".to_string(),
"parameter_group_family".to_string(),
version.matched_version().to_string(),
Some(e),
))
}
};
context.insert("parameter_group_family", &parameter_group_family);
};
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("encrypt_disk", &self.options.encrypt_disk);
context.insert("database_name", &self.sanitized_name());
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("skip_final_snapshot", &false);
context.insert("final_snapshot_name", &aws_final_snapshot_name(self.id()));
context.insert("delete_automated_backups", &self.context().is_test_cluster());
context.insert("publicly_accessible", &self.options.publicly_accessible);
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for MySQLAws {}
impl Helm for MySQLAws {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("mysql-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/mysql", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/aws/chart_values/mysql", self.context.lib_root_dir()) // FIXME replace `chart_values` by `charts_values`
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for MySQLAws {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/aws/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/aws/services/mysql", self.context.lib_root_dir())
}
}
impl Create for MySQLAws {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_create_check(&self) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
self.check_domains(self.listeners.clone(), vec![self.fqdn.as_str()], event_details, self.logger())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for MySQLAws {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for MySQLAws {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for MySQLAws {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}
fn get_mysql_version(requested_version: String, is_managed_service: bool) -> Result<String, CommandError> {
if is_managed_service {
get_managed_mysql_version(requested_version)
} else {
get_self_hosted_mysql_version(requested_version)
}
}
fn get_managed_mysql_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_mysql_versions = HashMap::new();
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_MySQL.html#MySQL.Concepts.VersionMgmt
// v5.7
let mut v57 = generate_supported_version(5, 7, 7, Some(16), Some(34), None);
v57.remove("5.7.32");
v57.remove("5.7.29");
v57.remove("5.7.27");
v57.remove("5.7.20");
v57.remove("5.7.18");
supported_mysql_versions.extend(v57);
// v8
let mut v8 = generate_supported_version(8, 0, 0, Some(11), Some(26), None);
v8.remove("8.0.24");
v8.remove("8.0.22");
v8.remove("8.0.18");
v8.remove("8.0.14");
v8.remove("8.0.12");
supported_mysql_versions.extend(v8);
get_supported_version_to_use("RDS MySQL", supported_mysql_versions, requested_version)
}
#[cfg(test)]
mod tests_mysql {
use crate::cloud_provider::aws::databases::mysql::get_mysql_version;
use crate::errors::ErrorMessageVerbosity;
#[test]
fn check_mysql_version() {
// managed version
assert_eq!(get_mysql_version("8".to_string(), true).unwrap(), "8.0.26");
assert_eq!(get_mysql_version("8.0".to_string(), true).unwrap(), "8.0.26");
assert_eq!(get_mysql_version("8.0.16".to_string(), true).unwrap(), "8.0.16");
assert!(get_mysql_version("8.0.18".to_string(), true)
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("RDS MySQL 8.0.18 version is not supported"));
// self-hosted version
assert_eq!(get_mysql_version("5".to_string(), false).unwrap(), "5.7.34");
assert_eq!(get_mysql_version("5.7".to_string(), false).unwrap(), "5.7.34");
assert_eq!(get_mysql_version("5.7.31".to_string(), false).unwrap(), "5.7.31");
assert!(get_mysql_version("1.0".to_string(), false)
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("MySQL 1.0 version is not supported"));
}
}

View File

@@ -1,468 +0,0 @@
use std::collections::HashMap;
use crate::cloud_provider::aws::databases::utilities::aws_final_snapshot_name;
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{
generate_supported_version, get_self_hosted_postgres_version, get_supported_version_to_use,
managed_db_name_sanitizer, print_action,
};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::{CommandError, EngineError};
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct PostgreSQLAws {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl PostgreSQLAws {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
PostgreSQLAws {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(
&self,
is_managed_services: bool,
event_details: EventDetails,
) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_postgres_version(self.version(), is_managed_services),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"aws"
}
fn struct_name(&self) -> &str {
"postgresql"
}
}
impl StatefulService for PostgreSQLAws {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for PostgreSQLAws {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for PostgreSQLAws {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::PostgreSQL(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html#RDS_Limits.Constraints
let prefix = "postgresql";
let max_size = 63 - 3; // max RDS - k8s statefulset chars
managed_db_name_sanitizer(max_size, prefix, self.name())
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Default
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = self
.matching_correct_version(self.is_managed_service(), event_details)?
.matched_version()
.to_string();
context.insert("version", &version);
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_name", self.sanitized_name().as_str());
context.insert("database_db_name", self.name());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("encrypt_disk", &self.options.encrypt_disk);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("skip_final_snapshot", &false);
context.insert("final_snapshot_name", &aws_final_snapshot_name(self.id()));
context.insert("delete_automated_backups", &self.context().is_test_cluster());
context.insert("publicly_accessible", &self.options.publicly_accessible);
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for PostgreSQLAws {}
impl Helm for PostgreSQLAws {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("postgresql-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/postgresql", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/aws/chart_values/postgresql", self.context.lib_root_dir()) // FIXME replace `chart_values` by `charts_values`
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for PostgreSQLAws {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/aws/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/aws/services/postgresql", self.context.lib_root_dir())
}
}
impl Create for PostgreSQLAws {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_create_check(&self) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
self.check_domains(self.listeners.clone(), vec![self.fqdn.as_str()], event_details, self.logger())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for PostgreSQLAws {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for PostgreSQLAws {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for PostgreSQLAws {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}
fn get_postgres_version(requested_version: String, is_managed_service: bool) -> Result<String, CommandError> {
if is_managed_service {
get_managed_postgres_version(requested_version)
} else {
get_self_hosted_postgres_version(requested_version)
}
}
fn get_managed_postgres_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_postgres_versions = HashMap::new();
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html#PostgreSQL.Concepts
// v10
let mut v10 = generate_supported_version(10, 1, 18, None, None, None);
v10.remove("10.2"); // non supported version by AWS
v10.remove("10.8"); // non supported version by AWS
supported_postgres_versions.extend(v10);
// v11
let mut v11 = generate_supported_version(11, 1, 13, None, None, None);
v11.remove("11.3"); // non supported version by AWS
supported_postgres_versions.extend(v11);
// v12
let v12 = generate_supported_version(12, 2, 8, None, None, None);
supported_postgres_versions.extend(v12);
// v13
let v13 = generate_supported_version(13, 1, 4, None, None, None);
supported_postgres_versions.extend(v13);
get_supported_version_to_use("Postgresql", supported_postgres_versions, requested_version)
}
#[cfg(test)]
mod tests_postgres {
use crate::cloud_provider::aws::databases::postgresql::get_postgres_version;
use crate::errors::ErrorMessageVerbosity;
#[test]
fn check_postgres_version() {
// managed version
assert_eq!(get_postgres_version("12".to_string(), true).unwrap(), "12.8");
assert_eq!(get_postgres_version("12.3".to_string(), true).unwrap(), "12.3");
assert!(get_postgres_version("12.3.0".to_string(), true)
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("Postgresql 12.3.0 version is not supported"));
assert!(get_postgres_version("11.3".to_string(), true)
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("Postgresql 11.3 version is not supported"));
// self-hosted version
assert_eq!(get_postgres_version("12".to_string(), false).unwrap(), "12.8.0");
assert_eq!(get_postgres_version("12.8".to_string(), false).unwrap(), "12.8.0");
assert_eq!(get_postgres_version("12.3.0".to_string(), false).unwrap(), "12.3.0");
assert!(get_postgres_version("1.0".to_string(), false)
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("Postgresql 1.0 version is not supported"));
}
}

View File

@@ -1,461 +0,0 @@
use std::collections::HashMap;
use crate::cloud_provider::aws::databases::utilities::aws_final_snapshot_name;
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{get_self_hosted_redis_version, get_supported_version_to_use, print_action};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::{CommandError, EngineError};
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct RedisAws {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl RedisAws {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
Self {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(
&self,
is_managed_services: bool,
event_details: EventDetails,
) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_redis_version(self.version(), is_managed_services),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"aws"
}
fn struct_name(&self) -> &str {
"redis"
}
}
impl StatefulService for RedisAws {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for RedisAws {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for RedisAws {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::Redis(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
// https://aws.amazon.com/about-aws/whats-new/2019/08/elasticache_supports_50_chars_cluster_name
let prefix = "redis";
let max_size = 47 - prefix.len(); // 50 (max Elasticache ) - 3 (k8s statefulset chars)
let mut new_name = self.name().replace('_', "").replace('-', "");
if new_name.chars().count() > max_size {
new_name = new_name[..max_size].to_string();
}
format!("{}{}", prefix, new_name)
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Default
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
let version = self
.matching_correct_version(self.is_managed_service(), event_details.clone())?
.matched_version()
.to_string();
let parameter_group_name = if version.starts_with("5.") {
"default.redis5.0"
} else if version.starts_with("6.") {
"default.redis6.x"
} else {
return Err(EngineError::new_terraform_unsupported_context_parameter_value(
event_details,
"Elasicache".to_string(),
"database_elasticache_parameter_group_name".to_string(),
format!("default.redis{}", version),
None,
));
};
context.insert("database_elasticache_parameter_group_name", parameter_group_name);
context.insert("namespace", environment.namespace());
context.insert("version", version.as_str());
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
context.insert("skip_final_snapshot", &false);
context.insert("final_snapshot_name", &aws_final_snapshot_name(self.id()));
context.insert("delete_automated_backups", &self.context().is_test_cluster());
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for RedisAws {}
impl Helm for RedisAws {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("redis-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/redis", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/aws/chart_values/redis", self.context.lib_root_dir()) // FIXME replace `chart_values` by `charts_values`
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for RedisAws {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/aws/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/aws/services/redis", self.context.lib_root_dir())
}
}
impl Create for RedisAws {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_create_check(&self) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
self.check_domains(self.listeners.clone(), vec![self.fqdn.as_str()], event_details, self.logger())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for RedisAws {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for RedisAws {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for RedisAws {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}
fn get_redis_version(requested_version: String, is_managed_service: bool) -> Result<String, CommandError> {
if is_managed_service {
get_managed_redis_version(requested_version)
} else {
get_self_hosted_redis_version(requested_version)
}
}
fn get_managed_redis_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_redis_versions = HashMap::with_capacity(2);
// https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/supported-engine-versions.html
supported_redis_versions.insert("6".to_string(), "6.x".to_string());
supported_redis_versions.insert("5".to_string(), "5.0.6".to_string());
get_supported_version_to_use("Elasticache", supported_redis_versions, requested_version)
}
#[cfg(test)]
mod tests {
use crate::cloud_provider::aws::databases::redis::get_redis_version;
use crate::errors::ErrorMessageVerbosity;
#[test]
fn check_redis_version() {
// managed version
assert_eq!(get_redis_version("6".to_string(), true).unwrap(), "6.x");
assert_eq!(get_redis_version("5".to_string(), true).unwrap(), "5.0.6");
assert!(get_redis_version("1.0".to_string(), true)
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("Elasticache 1.0 version is not supported"));
// self-hosted version
assert_eq!(get_redis_version("6".to_string(), false).unwrap(), "6.0.9");
assert_eq!(get_redis_version("6.0".to_string(), false).unwrap(), "6.0.9");
assert!(get_redis_version("1.0".to_string(), false)
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("Redis 1.0 version is not supported"));
}
}

View File

@@ -1,58 +0,0 @@
use crate::cloud_provider::utilities::VersionsNumber;
use crate::errors::CommandError;
use crate::io_models::DatabaseKind;
pub fn get_parameter_group_from_version(
version: VersionsNumber,
database_kind: DatabaseKind,
) -> Result<String, CommandError> {
if version.minor.is_none() {
return Err(CommandError::new_from_safe_message(format!(
"Can't determine the minor version, to select parameter group for {:?} version {}",
database_kind, version
)));
};
match database_kind {
DatabaseKind::Mysql => Ok(format!("mysql{}.{}", version.major, version.minor.unwrap())),
_ => Ok("".to_string()),
}
}
// name of the last snapshot before the database get deleted
pub fn aws_final_snapshot_name(database_name: &str) -> String {
format!("qovery-{}-final-snap", database_name)
}
#[cfg(test)]
mod tests_aws_databases_parameters {
use crate::cloud_provider::aws::databases::utilities::get_parameter_group_from_version;
use crate::cloud_provider::utilities::VersionsNumber;
use crate::errors::ErrorMessageVerbosity;
use crate::io_models::DatabaseKind;
use std::str::FromStr;
#[test]
fn check_rds_mysql_parameter_groups() {
let mysql_parameter_group = get_parameter_group_from_version(
VersionsNumber::from_str("5.7.0").expect("error while trying to get version from str"),
DatabaseKind::Mysql,
);
assert_eq!(mysql_parameter_group.unwrap(), "mysql5.7");
let mysql_parameter_group = get_parameter_group_from_version(
VersionsNumber::from_str("8.0").expect("error while trying to get version from str"),
DatabaseKind::Mysql,
);
assert_eq!(mysql_parameter_group.unwrap(), "mysql8.0");
let mysql_parameter_group = get_parameter_group_from_version(
VersionsNumber::from_str("8").expect("error while trying to get version from str"),
DatabaseKind::Mysql,
);
assert!(mysql_parameter_group
.unwrap_err()
.message(ErrorMessageVerbosity::FullDetails)
.contains("Can't determine the minor version, to select parameter group for Mysql version 8"));
}
}

View File

@@ -12,7 +12,6 @@ use crate::events::{EventDetails, GeneralStep, Stage, ToTransmitter, Transmitter
use crate::io_models::{Context, Listen, Listener, Listeners, QoveryIdentifier};
use crate::runtime::block_on;
pub mod databases;
pub mod kubernetes;
pub mod regions;

View File

@@ -1,4 +0,0 @@
pub mod mongodb;
pub mod mysql;
pub mod postgresql;
pub mod redis;

View File

@@ -1,384 +0,0 @@
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{get_self_hosted_mongodb_version, print_action, sanitize_name};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::EngineError;
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct MongoDo {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl MongoDo {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
MongoDo {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(&self, event_details: EventDetails) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_self_hosted_mongodb_version(self.version()),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"digitalocean"
}
fn struct_name(&self) -> &str {
"mongodb"
}
}
impl StatefulService for MongoDo {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for MongoDo {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for MongoDo {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::MongoDB(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
sanitize_name("mongodb", self.name())
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Value(600)
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = self
.matching_correct_version(event_details)?
.matched_version()
.to_string();
context.insert("version", &version);
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_db_name", self.name.as_str());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for MongoDo {}
impl Helm for MongoDo {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("mongodb-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/mongodb", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/digitalocean/chart_values/mongodb", self.context.lib_root_dir())
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for MongoDo {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/digitalocean/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/digitalocean/services/mongodb", self.context.lib_root_dir())
}
}
impl Create for MongoDo {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_create_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for MongoDo {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for MongoDo {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for MongoDo {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}

View File

@@ -1,388 +0,0 @@
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{get_self_hosted_mysql_version, print_action, sanitize_name};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::EngineError;
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct MySQLDo {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl MySQLDo {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
Self {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(&self, event_details: EventDetails) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_self_hosted_mysql_version(self.version()),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"digitalocean"
}
fn struct_name(&self) -> &str {
"mysql"
}
}
impl StatefulService for MySQLDo {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for MySQLDo {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for MySQLDo {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::MySQL(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
sanitize_name("mysql", self.name())
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Value(600)
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = &self
.matching_correct_version(event_details)?
.matched_version()
.to_string();
context.insert("version", &version);
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
context.insert("delete_automated_backups", &self.context().is_test_cluster());
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for MySQLDo {}
impl Helm for MySQLDo {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("mysql-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/mysql", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/digitalocean/chart_values/mysql", self.context.lib_root_dir())
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for MySQLDo {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/digitalocean/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/digitalocean/services/mysql", self.context.lib_root_dir())
}
}
impl Create for MySQLDo {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(
self,
crate::cloud_provider::service::Action::Create,
Box::new(|| deploy_stateful_service(target, self, event_details.clone(), self.logger())),
)
}
fn on_create_check(&self) -> Result<(), EngineError> {
//FIXME : perform an actual check
Ok(())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for MySQLDo {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for MySQLDo {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(
self,
crate::cloud_provider::service::Action::Delete,
Box::new(|| delete_stateful_service(target, self, event_details.clone(), self.logger())),
)
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for MySQLDo {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}

View File

@@ -1,390 +0,0 @@
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{get_self_hosted_postgres_version, print_action, sanitize_name};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::EngineError;
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct PostgresDo {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl PostgresDo {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
PostgresDo {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(&self, event_details: EventDetails) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_self_hosted_postgres_version(self.version()),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"digitalocean"
}
fn struct_name(&self) -> &str {
"postgresql"
}
}
impl StatefulService for PostgresDo {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for PostgresDo {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for PostgresDo {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::PostgreSQL(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
sanitize_name("postgresql", self.name())
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Value(600)
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = self
.matching_correct_version(event_details)?
.matched_version()
.to_string();
context.insert("version", &version);
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_db_name", self.name());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
context.insert("delete_automated_backups", &self.context().is_test_cluster());
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
}
impl Database for PostgresDo {}
impl Helm for PostgresDo {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("postgresql-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/postgresql", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/digitalocean/chart_values/postgresql", self.context.lib_root_dir())
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for PostgresDo {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/digitalocean/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/digitalocean/services/postgresql", self.context.lib_root_dir())
}
}
impl Create for PostgresDo {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(
self,
crate::cloud_provider::service::Action::Create,
Box::new(|| deploy_stateful_service(target, self, event_details.clone(), self.logger())),
)
}
fn on_create_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for PostgresDo {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for PostgresDo {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(
self,
crate::cloud_provider::service::Action::Delete,
Box::new(|| delete_stateful_service(target, self, event_details.clone(), self.logger())),
)
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for PostgresDo {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}

View File

@@ -1,385 +0,0 @@
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{get_self_hosted_redis_version, print_action, sanitize_name};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::EngineError;
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct RedisDo {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl RedisDo {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
Self {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(&self, event_details: EventDetails) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_self_hosted_redis_version(self.version()),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"digitalocean"
}
fn struct_name(&self) -> &str {
"redis"
}
}
impl StatefulService for RedisDo {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for RedisDo {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for RedisDo {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::Redis(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
sanitize_name("redis", self.name())
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Value(600)
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
let version = self
.matching_correct_version(event_details)?
.matched_version()
.to_string();
context.insert("namespace", environment.namespace());
context.insert("version", &version);
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for RedisDo {}
impl Helm for RedisDo {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("redis-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/redis", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/digitalocean/chart_values/redis", self.context.lib_root_dir())
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for RedisDo {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/digitalocean/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/digitalocean/services/redis", self.context.lib_root_dir())
}
}
impl Create for RedisDo {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(
self,
crate::cloud_provider::service::Action::Create,
Box::new(|| deploy_stateful_service(target, self, event_details.clone(), self.logger())),
)
}
fn on_create_check(&self) -> Result<(), EngineError> {
//FIXME : perform an actual check
Ok(())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for RedisDo {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for RedisDo {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(
self,
crate::cloud_provider::service::Action::Pause,
Box::new(|| delete_stateful_service(target, self, event_details.clone(), self.logger())),
)
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for RedisDo {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}

View File

@@ -1,8 +1,8 @@
use crate::cloud_provider::digitalocean::do_api_common::{do_get_from_api, DoApiType};
use crate::cloud_provider::digitalocean::models::doks::KubernetesCluster;
use crate::cloud_provider::digitalocean::models::doks::{DoksList, DoksOptions, KubernetesVersion};
use crate::cloud_provider::utilities::VersionsNumber;
use crate::errors::CommandError;
use crate::models::types::VersionsNumber;
use std::str::FromStr;
pub fn get_doks_info_from_name(

View File

@@ -25,7 +25,7 @@ use crate::cloud_provider::kubernetes::{
};
use crate::cloud_provider::models::NodeGroups;
use crate::cloud_provider::qovery::EngineLocation;
use crate::cloud_provider::utilities::{print_action, VersionsNumber};
use crate::cloud_provider::utilities::print_action;
use crate::cloud_provider::{kubernetes, CloudProvider};
use crate::cmd::helm::{to_engine_error, Helm};
use crate::cmd::kubectl::{
@@ -45,6 +45,7 @@ use crate::io_models::{
};
use crate::logger::Logger;
use crate::models::digital_ocean::DoRegion;
use crate::models::types::VersionsNumber;
use crate::object_storage::spaces::{BucketDeleteStrategy, Spaces};
use crate::object_storage::ObjectStorage;
use crate::runtime::block_on;

View File

@@ -11,7 +11,6 @@ use crate::errors::EngineError;
use crate::events::{EventDetails, GeneralStep, Stage, ToTransmitter, Transmitter};
use crate::io_models::{Context, Listen, Listener, Listeners, QoveryIdentifier};
pub mod databases;
pub mod do_api_common;
pub mod kubernetes;
pub mod models;

View File

@@ -18,7 +18,6 @@ use crate::cloud_provider::aws::regions::AwsZones;
use crate::cloud_provider::environment::Environment;
use crate::cloud_provider::models::{CpuLimits, NodeGroups};
use crate::cloud_provider::service::CheckAction;
use crate::cloud_provider::utilities::VersionsNumber;
use crate::cloud_provider::{service, CloudProvider, DeploymentTarget};
use crate::cmd::kubectl;
use crate::cmd::kubectl::{
@@ -36,6 +35,7 @@ use crate::io_models::{
Action, Context, Listen, ListenersHelper, ProgressInfo, ProgressLevel, ProgressScope, QoveryIdentifier, StringPath,
};
use crate::logger::Logger;
use crate::models::types::VersionsNumber;
use crate::object_storage::ObjectStorage;
use crate::unit_conversion::{any_to_mi, cpu_string_to_float};
@@ -1425,18 +1425,18 @@ pub fn convert_k8s_cpu_value_to_f32(value: String) -> Result<f32, CommandError>
#[cfg(test)]
mod tests {
use crate::cloud_provider::Kind::Aws;
use std::str::FromStr;
use crate::cloud_provider::kubernetes::{
check_kubernetes_upgrade_status, compare_kubernetes_cluster_versions_for_upgrade, convert_k8s_cpu_value_to_f32,
validate_k8s_required_cpu_and_burstable, KubernetesNodesType,
};
use crate::cloud_provider::models::CpuLimits;
use crate::cloud_provider::utilities::VersionsNumber;
use crate::cmd::structs::{KubernetesList, KubernetesNode, KubernetesVersion};
use crate::events::{EventDetails, InfrastructureStep, Stage, Transmitter};
use crate::io_models::{ListenersHelper, QoveryIdentifier};
use crate::logger::StdIoLogger;
use crate::models::types::VersionsNumber;
use std::str::FromStr;
#[test]
pub fn check_kubernetes_upgrade_method() {

View File

@@ -1,4 +0,0 @@
pub mod mongodb;
pub mod mysql;
pub mod postgresql;
pub mod redis;

View File

@@ -1,385 +0,0 @@
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{get_self_hosted_mongodb_version, print_action, sanitize_name};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::EngineError;
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct MongoDbScw {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl MongoDbScw {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
MongoDbScw {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(&self, event_details: EventDetails) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_self_hosted_mongodb_version(self.version()),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"scaleway"
}
fn struct_name(&self) -> &str {
"mongodb"
}
}
impl StatefulService for MongoDbScw {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for MongoDbScw {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for MongoDbScw {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::MongoDB(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
sanitize_name("mongodb", self.name())
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Value(600)
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = self
.matching_correct_version(event_details)?
.matched_version()
.to_string();
context.insert("version", &version);
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_db_name", self.name.as_str());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for MongoDbScw {}
impl Helm for MongoDbScw {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("mongodb-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/mongodb", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/scaleway/chart_values/mongodb", self.context.lib_root_dir())
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for MongoDbScw {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/scaleway/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/scaleway/services/mongodb", self.context.lib_root_dir())
}
}
impl Create for MongoDbScw {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_create_check(&self) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
self.check_domains(self.listeners.clone(), vec![self.fqdn.as_str()], event_details, self.logger())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for MongoDbScw {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for MongoDbScw {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for MongoDbScw {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}

View File

@@ -1,417 +0,0 @@
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{
get_self_hosted_mysql_version, get_supported_version_to_use, print_action, sanitize_name, VersionsNumber,
};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::{CommandError, EngineError};
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
use std::collections::HashMap;
pub struct MySQLScw {
context: Context,
id: String,
action: Action,
name: String,
version: VersionsNumber,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl MySQLScw {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: VersionsNumber,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
Self {
context,
action,
id: id.to_string(),
name: name.to_string(),
version,
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(
&self,
is_managed_services: bool,
event_details: EventDetails,
) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
Self::pick_mysql_version(self.version(), is_managed_services),
self,
event_details,
self.logger(),
)
}
fn pick_mysql_version(requested_version: String, is_managed_service: bool) -> Result<String, CommandError> {
if is_managed_service {
Self::pick_managed_mysql_version(requested_version)
} else {
get_self_hosted_mysql_version(requested_version)
}
}
fn pick_managed_mysql_version(requested_version: String) -> Result<String, CommandError> {
// Scaleway supported MySQL versions
// https://api.scaleway.com/rdb/v1/regions/fr-par/database-engines
let mut supported_mysql_versions = HashMap::new();
// {"name": "MySQL", "version":"8","end_of_life":"2026-04-01T00:00:00Z"}
supported_mysql_versions.insert("8".to_string(), "8".to_string());
supported_mysql_versions.insert("8.0".to_string(), "8.0".to_string());
get_supported_version_to_use("RDB MySQL", supported_mysql_versions, requested_version)
}
fn cloud_provider_name(&self) -> &str {
"scaleway"
}
fn struct_name(&self) -> &str {
"mysql"
}
}
impl StatefulService for MySQLScw {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for MySQLScw {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for MySQLScw {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::MySQL(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
sanitize_name("mysql", self.name())
}
fn version(&self) -> String {
self.version.to_string()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Value(600)
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = &self
.matching_correct_version(self.is_managed_service(), event_details)?
.matched_version();
context.insert("version_major", &version.to_major_version_string());
context.insert("version", &version.to_string()); // Scaleway needs to have major version only
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("database_name", &self.sanitized_name());
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
context.insert("activate_high_availability", &self.options.activate_high_availability);
context.insert("activate_backups", &self.options.activate_backups);
context.insert("delete_automated_backups", &self.context().is_test_cluster());
context.insert("skip_final_snapshot", &self.context().is_test_cluster());
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for MySQLScw {}
impl Helm for MySQLScw {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("mysql-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/mysql", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/scaleway/chart_values/mysql", self.context.lib_root_dir())
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for MySQLScw {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/scaleway/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/scaleway/services/mysql", self.context.lib_root_dir())
}
}
impl Create for MySQLScw {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_create_check(&self) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
self.check_domains(self.listeners.clone(), vec![self.fqdn.as_str()], event_details, self.logger())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for MySQLScw {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for MySQLScw {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for MySQLScw {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}

View File

@@ -1,426 +0,0 @@
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{
get_self_hosted_postgres_version, get_supported_version_to_use, print_action, sanitize_name, VersionsNumber,
};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::{CommandError, EngineError};
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
use std::collections::HashMap;
pub struct PostgresScw {
context: Context,
id: String,
action: Action,
name: String,
version: VersionsNumber,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl PostgresScw {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: VersionsNumber,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
Self {
context,
action,
id: id.to_string(),
name: name.to_string(),
version,
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(
&self,
is_managed_services: bool,
event_details: EventDetails,
) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
Self::pick_postgres_version(self.version(), is_managed_services),
self,
event_details,
self.logger(),
)
}
fn pick_postgres_version(requested_version: String, is_managed_service: bool) -> Result<String, CommandError> {
if is_managed_service {
Self::pick_managed_postgres_version(requested_version)
} else {
get_self_hosted_postgres_version(requested_version)
}
}
fn pick_managed_postgres_version(requested_version: String) -> Result<String, CommandError> {
// Scaleway supported postgres versions
// https://api.scaleway.com/rdb/v1/regions/fr-par/database-engines
let mut supported_postgres_versions = HashMap::new();
// {"name":"PostgreSQL","version":"13","end_of_life":"2025-11-13T00:00:00Z"}
// {"name":"PostgreSQL","version":"12","end_of_life":"2024-11-14T00:00:00Z"}
// {"name":"PostgreSQL","version":"11","end_of_life":"2023-11-09T00:00:00Z"}
// {"name":"PostgreSQL","version":"10","end_of_life":"2022-11-10T00:00:00Z"}
supported_postgres_versions.insert("10".to_string(), "10".to_string());
supported_postgres_versions.insert("10.0".to_string(), "10.0".to_string());
supported_postgres_versions.insert("11".to_string(), "11".to_string());
supported_postgres_versions.insert("11.0".to_string(), "11.0".to_string());
supported_postgres_versions.insert("12".to_string(), "12".to_string());
supported_postgres_versions.insert("12.0".to_string(), "12.0".to_string());
supported_postgres_versions.insert("13".to_string(), "13".to_string());
supported_postgres_versions.insert("13.0".to_string(), "13.0".to_string());
get_supported_version_to_use("RDB postgres", supported_postgres_versions, requested_version)
}
fn cloud_provider_name(&self) -> &str {
"scaleway"
}
fn struct_name(&self) -> &str {
"postgresql"
}
}
impl StatefulService for PostgresScw {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for PostgresScw {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for PostgresScw {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::PostgreSQL(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
sanitize_name("postgresql", self.name())
}
fn version(&self) -> String {
self.version.to_string()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Value(600)
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = &self
.matching_correct_version(self.is_managed_service(), event_details)?
.matched_version();
context.insert("version_major", &version.to_major_version_string());
context.insert("version", &version.to_string()); // Scaleway needs to have major version only
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_name", self.sanitized_name().as_str());
context.insert("database_db_name", self.name());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
context.insert("activate_high_availability", &self.options.activate_high_availability);
context.insert("activate_backups", &self.options.activate_backups);
context.insert("delete_automated_backups", &self.context().is_test_cluster());
context.insert("skip_final_snapshot", &self.context().is_test_cluster());
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for PostgresScw {}
impl Helm for PostgresScw {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("postgresql-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/postgresql", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/scaleway/chart_values/postgresql", self.context.lib_root_dir())
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for PostgresScw {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/scaleway/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/scaleway/services/postgresql", self.context.lib_root_dir())
}
}
impl Create for PostgresScw {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_create_check(&self) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
self.check_domains(self.listeners.clone(), vec![self.fqdn.as_str()], event_details, self.logger())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for PostgresScw {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for PostgresScw {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for PostgresScw {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}

View File

@@ -1,382 +0,0 @@
use tera::Context as TeraContext;
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, Database, DatabaseOptions,
DatabaseType, Delete, Helm, Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{get_self_hosted_redis_version, print_action, sanitize_name};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::EngineError;
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::DatabaseMode::MANAGED;
use crate::io_models::{Context, Listen, Listener, Listeners};
use crate::logger::Logger;
use ::function_name::named;
pub struct RedisScw {
context: Context,
id: String,
action: Action,
name: String,
version: String,
fqdn: String,
fqdn_id: String,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: String,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
}
impl RedisScw {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: &str,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
options: DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Self {
Self {
context,
action,
id: id.to_string(),
name: name.to_string(),
version: version.to_string(),
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
options,
listeners,
logger,
}
}
fn matching_correct_version(&self, event_details: EventDetails) -> Result<ServiceVersionCheckResult, EngineError> {
check_service_version(
get_self_hosted_redis_version(self.version()),
self,
event_details,
self.logger(),
)
}
fn cloud_provider_name(&self) -> &str {
"scaleway"
}
fn struct_name(&self) -> &str {
"redis"
}
}
impl StatefulService for RedisScw {
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
self.options.mode == MANAGED
}
}
impl ToTransmitter for RedisScw {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id().to_string(), self.service_type().to_string(), self.name().to_string())
}
}
impl Service for RedisScw {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(DatabaseType::Redis(&self.options))
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn sanitized_name(&self) -> String {
sanitize_name("redis", self.name())
}
fn version(&self) -> String {
self.version.clone()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.options.port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Value(600)
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
unimplemented!()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.options.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
let version = self
.matching_correct_version(event_details)?
.matched_version()
.to_string();
context.insert("namespace", environment.namespace());
context.insert("version", &version);
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, self.is_managed_service()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_login", self.options.login.as_str());
context.insert("database_password", self.options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &self.options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &self.options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &self.options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.options.publicly_accessible);
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
fn logger(&self) -> &dyn Logger {
&*self.logger
}
fn selector(&self) -> Option<String> {
Some(format!("app={}", self.sanitized_name()))
}
}
impl Database for RedisScw {}
impl Helm for RedisScw {
fn helm_selector(&self) -> Option<String> {
self.selector()
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("redis-{}", self.id()), 50)
}
fn helm_chart_dir(&self) -> String {
format!("{}/common/services/redis", self.context.lib_root_dir())
}
fn helm_chart_values_dir(&self) -> String {
format!("{}/scaleway/chart_values/redis", self.context.lib_root_dir())
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl Terraform for RedisScw {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/scaleway/services/common", self.context.lib_root_dir())
}
fn terraform_resource_dir_path(&self) -> String {
format!("{}/scaleway/services/redis", self.context.lib_root_dir())
}
}
impl Create for RedisScw {
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_create_check(&self) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
self.check_domains(self.listeners.clone(), vec![self.fqdn.as_str()], event_details, self.logger())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Pause for RedisScw {
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Pause, || {
scale_down_database(target, self, 0)
})
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Delete for RedisScw {
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, crate::cloud_provider::service::Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
self.cloud_provider_name(),
self.struct_name(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl Listen for RedisScw {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}

View File

@@ -6,7 +6,6 @@ use crate::constants::{SCALEWAY_ACCESS_KEY, SCALEWAY_DEFAULT_PROJECT_ID, SCALEWA
use crate::events::{EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::{Context, Listen, Listener, Listeners, QoveryIdentifier};
pub mod databases;
pub mod kubernetes;
pub struct Scaleway {

View File

@@ -10,7 +10,7 @@ use tera::Context as TeraContext;
use crate::cloud_provider::environment::Environment;
use crate::cloud_provider::helm::ChartInfo;
use crate::cloud_provider::kubernetes::Kubernetes;
use crate::cloud_provider::utilities::{check_domain_for, VersionsNumber};
use crate::cloud_provider::utilities::check_domain_for;
use crate::cloud_provider::DeploymentTarget;
use crate::cmd;
use crate::cmd::helm;
@@ -26,6 +26,7 @@ use crate::io_models::{
QoveryIdentifier,
};
use crate::logger::Logger;
use crate::models::types::VersionsNumber;
pub trait Service: ToTransmitter {
fn context(&self) -> &Context;
@@ -250,32 +251,32 @@ pub struct DatabaseOptions {
}
#[derive(Eq, PartialEq)]
pub enum DatabaseType<'a> {
PostgreSQL(&'a DatabaseOptions),
MongoDB(&'a DatabaseOptions),
MySQL(&'a DatabaseOptions),
Redis(&'a DatabaseOptions),
pub enum DatabaseType {
PostgreSQL,
MongoDB,
MySQL,
Redis,
}
impl<'a> ToString for DatabaseType<'a> {
impl ToString for DatabaseType {
fn to_string(&self) -> String {
match self {
DatabaseType::PostgreSQL(_) => "PostgreSQL".to_string(),
DatabaseType::MongoDB(_) => "MongoDB".to_string(),
DatabaseType::MySQL(_) => "MySQL".to_string(),
DatabaseType::Redis(_) => "Redis".to_string(),
DatabaseType::PostgreSQL => "PostgreSQL".to_string(),
DatabaseType::MongoDB => "MongoDB".to_string(),
DatabaseType::MySQL => "MySQL".to_string(),
DatabaseType::Redis => "Redis".to_string(),
}
}
}
#[derive(Eq, PartialEq)]
pub enum ServiceType<'a> {
pub enum ServiceType {
Application,
Database(DatabaseType<'a>),
Database(DatabaseType),
Router,
}
impl<'a> ServiceType<'a> {
impl ServiceType {
pub fn name(&self) -> String {
match self {
ServiceType::Application => "Application".to_string(),
@@ -285,7 +286,7 @@ impl<'a> ServiceType<'a> {
}
}
impl<'a> ToString for ServiceType<'a> {
impl<'a> ToString for ServiceType {
fn to_string(&self) -> String {
self.name()
}

View File

@@ -1,8 +1,6 @@
#![allow(clippy::field_reassign_with_default)]
use std::collections::HashMap;
use crate::errors::{CommandError, EngineError};
use crate::errors::EngineError;
use crate::events::{EngineEvent, EventDetails, EventMessage};
use crate::io_models::{Listeners, ListenersHelper, ProgressInfo, ProgressLevel, ProgressScope};
use crate::logger::Logger;
@@ -12,306 +10,10 @@ use core::result::Result;
use core::result::Result::{Err, Ok};
use retry::delay::Fixed;
use retry::OperationResult;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::Write;
use std::str::FromStr;
use trust_dns_resolver::config::*;
use trust_dns_resolver::proto::rr::{RData, RecordType};
use trust_dns_resolver::Resolver;
pub fn get_self_hosted_postgres_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_postgres_versions = HashMap::new();
// https://hub.docker.com/r/bitnami/postgresql/tags?page=1&ordering=last_updated
// v10
let v10 = generate_supported_version(10, 1, 16, Some(0), Some(0), None);
supported_postgres_versions.extend(v10);
// v11
let v11 = generate_supported_version(11, 1, 11, Some(0), Some(0), None);
supported_postgres_versions.extend(v11);
// v12
let v12 = generate_supported_version(12, 2, 8, Some(0), Some(0), None);
supported_postgres_versions.extend(v12);
// v13
let v13 = generate_supported_version(13, 1, 4, Some(0), Some(0), None);
supported_postgres_versions.extend(v13);
get_supported_version_to_use("Postgresql", supported_postgres_versions, requested_version)
}
pub fn get_self_hosted_mysql_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_mysql_versions = HashMap::new();
// https://hub.docker.com/r/bitnami/mysql/tags?page=1&ordering=last_updated
// v5.7
let v57 = generate_supported_version(5, 7, 7, Some(16), Some(34), None);
supported_mysql_versions.extend(v57);
// v8
let v8 = generate_supported_version(8, 0, 0, Some(11), Some(24), None);
supported_mysql_versions.extend(v8);
get_supported_version_to_use("MySQL", supported_mysql_versions, requested_version)
}
pub fn get_self_hosted_mongodb_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_mongodb_versions = HashMap::new();
// https://hub.docker.com/r/bitnami/mongodb/tags?page=1&ordering=last_updated
// v3.6
let mongo_version = generate_supported_version(3, 6, 6, Some(0), Some(22), None);
supported_mongodb_versions.extend(mongo_version);
// v4.0
let mongo_version = generate_supported_version(4, 0, 0, Some(0), Some(23), None);
supported_mongodb_versions.extend(mongo_version);
// v4.2
let mongo_version = generate_supported_version(4, 2, 2, Some(0), Some(12), None);
supported_mongodb_versions.extend(mongo_version);
// v4.4
let mongo_version = generate_supported_version(4, 4, 4, Some(0), Some(4), None);
supported_mongodb_versions.extend(mongo_version);
get_supported_version_to_use("MongoDB", supported_mongodb_versions, requested_version)
}
pub fn get_self_hosted_redis_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_redis_versions = HashMap::with_capacity(4);
// https://hub.docker.com/r/bitnami/redis/tags?page=1&ordering=last_updated
supported_redis_versions.insert("6".to_string(), "6.0.9".to_string());
supported_redis_versions.insert("6.0".to_string(), "6.0.9".to_string());
supported_redis_versions.insert("5".to_string(), "5.0.10".to_string());
supported_redis_versions.insert("5.0".to_string(), "5.0.10".to_string());
get_supported_version_to_use("Redis", supported_redis_versions, requested_version)
}
pub fn get_supported_version_to_use(
database_name: &str,
all_supported_versions: HashMap<String, String>,
version_to_check: String,
) -> Result<String, CommandError> {
let version = VersionsNumber::from_str(version_to_check.as_str())?;
// if a patch version is required
if version.patch.is_some() {
return match all_supported_versions.get(&format!(
"{}.{}.{}",
version.major,
version.minor.unwrap(),
version.patch.unwrap()
)) {
Some(version) => Ok(version.to_string()),
None => {
return Err(CommandError::new_from_safe_message(format!(
"{} {} version is not supported",
database_name, version_to_check
)));
}
};
}
// if a minor version is required
if version.minor.is_some() {
return match all_supported_versions.get(&format!("{}.{}", version.major, version.minor.unwrap())) {
Some(version) => Ok(version.to_string()),
None => {
return Err(CommandError::new_from_safe_message(format!(
"{} {} version is not supported",
database_name, version_to_check
)));
}
};
};
// if only a major version is required
match all_supported_versions.get(&version.major) {
Some(version) => Ok(version.to_string()),
None => {
return Err(CommandError::new_from_safe_message(format!(
"{} {} version is not supported",
database_name, version_to_check
)));
}
}
}
// Ease the support of multiple versions by range
pub fn generate_supported_version(
major: i32,
minor_min: i32,
minor_max: i32,
update_min: Option<i32>,
update_max: Option<i32>,
suffix_version: Option<String>,
) -> HashMap<String, String> {
let mut supported_versions = HashMap::new();
let latest_major_version;
// blank suffix if not requested
let suffix = match suffix_version {
Some(suffix) => suffix,
None => "".to_string(),
};
let _ = match update_min {
// manage minor with updates
Some(_) => {
latest_major_version = format!("{}.{}.{}{}", major, minor_max, update_max.unwrap(), suffix);
if minor_min == minor_max {
// add short minor format targeting latest version
supported_versions.insert(format!("{}.{}", major, minor_max), latest_major_version.clone());
if update_min.unwrap() == update_max.unwrap() {
let version = format!("{}.{}.{}", major, minor_min, update_min.unwrap());
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
} else {
for update in update_min.unwrap()..update_max.unwrap() + 1 {
let version = format!("{}.{}.{}", major, minor_min, update);
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
}
}
} else {
for minor in minor_min..minor_max + 1 {
// add short minor format targeting latest version
supported_versions.insert(
format!("{}.{}", major, minor),
format!("{}.{}.{}", major, minor, update_max.unwrap()),
);
if update_min.unwrap() == update_max.unwrap() {
let version = format!("{}.{}.{}", major, minor, update_min.unwrap());
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
} else {
for update in update_min.unwrap()..update_max.unwrap() + 1 {
let version = format!("{}.{}.{}", major, minor, update);
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
}
}
}
}
}
// manage minor without updates
None => {
latest_major_version = format!("{}.{}{}", major, minor_max, suffix);
for minor in minor_min..minor_max + 1 {
let version = format!("{}.{}", major, minor);
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
}
}
};
// default major + major.minor supported version
supported_versions.insert(major.to_string(), latest_major_version);
supported_versions
}
// unfortunately some proposed versions are not SemVer like Elasticache (6.x)
// this is why we need ot have our own structure
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct VersionsNumber {
pub(crate) major: String,
pub(crate) minor: Option<String>,
pub(crate) patch: Option<String>,
pub(crate) suffix: Option<String>,
}
impl VersionsNumber {
pub fn new(major: String, minor: Option<String>, patch: Option<String>, suffix: Option<String>) -> Self {
VersionsNumber {
major,
minor,
patch,
suffix,
}
}
pub fn to_major_version_string(&self) -> String {
self.major.clone()
}
pub fn to_major_minor_version_string(&self, default_minor: &str) -> String {
let test = format!(
"{}.{}",
self.major.clone(),
self.minor.as_ref().unwrap_or(&default_minor.to_string())
);
test
}
}
impl FromStr for VersionsNumber {
type Err = CommandError;
fn from_str(version: &str) -> Result<Self, Self::Err> {
if version.trim() == "" {
return Err(CommandError::new_from_safe_message("version cannot be empty".to_string()));
}
let mut version_split = version.splitn(4, '.').map(|v| v.trim());
let major = match version_split.next() {
Some(major) => {
let major = major.to_string();
major.replace('v', "")
}
None => {
return Err(CommandError::new_from_safe_message(format!(
"please check the version you've sent ({}), it can't be checked",
version
)))
}
};
let minor = version_split.next().map(|minor| {
let minor = minor.to_string();
minor.replace('+', "")
});
let patch = version_split.next().map(|patch| patch.to_string());
let suffix = version_split.next().map(|suffix| suffix.to_string());
// TODO(benjaminch): Handle properly the case where versions are empty
// eq. 1..2
Ok(VersionsNumber::new(major, minor, patch, suffix))
}
}
impl fmt::Display for VersionsNumber {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.major)?;
if let Some(minor) = &self.minor {
f.write_char('.')?;
f.write_str(minor)?;
}
if let Some(patch) = &self.patch {
f.write_char('.')?;
f.write_str(patch)?;
}
if let Some(suffix) = &self.suffix {
f.write_char('.')?;
f.write_str(suffix)?;
}
Ok(())
}
}
fn dns_resolvers() -> Vec<Resolver> {
let mut resolver_options = ResolverOpts::default();
@@ -559,8 +261,9 @@ pub fn print_action(
#[cfg(test)]
mod tests {
use crate::cloud_provider::utilities::{dns_resolvers, get_cname_record_value, VersionsNumber};
use crate::cloud_provider::utilities::{dns_resolvers, get_cname_record_value};
use crate::errors::CommandError;
use crate::models::types::VersionsNumber;
use std::str::FromStr;
#[test]

View File

@@ -3,7 +3,6 @@ pub mod io;
extern crate url;
use crate::build_platform::BuildError;
use crate::cloud_provider::utilities::VersionsNumber;
use crate::cmd;
use crate::cmd::docker::DockerError;
use crate::cmd::helm::HelmError;
@@ -11,6 +10,7 @@ use crate::container_registry::errors::ContainerRegistryError;
use crate::error::{EngineError as LegacyEngineError, EngineErrorCause, EngineErrorScope};
use crate::events::{EventDetails, GeneralStep, Stage, Transmitter};
use crate::io_models::QoveryIdentifier;
use crate::models::types::VersionsNumber;
use crate::object_storage::errors::ObjectStorageError;
use std::fmt::{Display, Formatter};
use thiserror::Error;

View File

@@ -14,34 +14,21 @@ use serde::{Deserialize, Serialize};
use url::Url;
use crate::build_platform::{Build, Credentials, GitRepository, Image, SshKey};
use crate::cloud_provider::aws::databases::mongodb::MongoDbAws;
use crate::cloud_provider::aws::databases::mysql::MySQLAws;
use crate::cloud_provider::aws::databases::postgresql::PostgreSQLAws;
use crate::cloud_provider::aws::databases::redis::RedisAws;
use crate::cloud_provider::digitalocean::databases::mongodb::MongoDo;
use crate::cloud_provider::digitalocean::databases::mysql::MySQLDo;
use crate::cloud_provider::digitalocean::databases::postgresql::PostgresDo;
use crate::cloud_provider::digitalocean::databases::redis::RedisDo;
use crate::cloud_provider::environment::Environment;
use crate::cloud_provider::scaleway::databases::mongodb::MongoDbScw;
use crate::cloud_provider::scaleway::databases::mysql::MySQLScw;
use crate::cloud_provider::scaleway::databases::postgresql::PostgresScw;
use crate::cloud_provider::scaleway::databases::redis::RedisScw;
use crate::cloud_provider::service::{DatabaseOptions, RouterService};
use crate::cloud_provider::utilities::VersionsNumber;
use crate::cloud_provider::CloudProvider;
use crate::cloud_provider::Kind as CPKind;
use crate::cmd::docker::Docker;
use crate::container_registry::ContainerRegistryInfo;
use crate::errors::ErrorMessageVerbosity;
use crate::logger::Logger;
use crate::models;
use crate::models::application::{ApplicationError, ApplicationService};
use crate::models::aws::{AwsAppExtraSettings, AwsRouterExtraSettings, AwsStorageType};
use crate::models::database::{Container, Managed, MongoDB, MySQL, PostgresSQL, Redis};
use crate::models::digital_ocean::{DoAppExtraSettings, DoRouterExtraSettings, DoStorageType};
use crate::models::router::RouterError;
use crate::models::scaleway::{ScwAppExtraSettings, ScwRouterExtraSettings, ScwStorageType};
use crate::models::types::{AWS, DO, SCW};
use crate::models::types::{VersionsNumber, AWS, DO, SCW};
#[derive(Clone, Debug, PartialEq)]
pub struct QoveryIdentifier {
@@ -651,265 +638,415 @@ impl Database {
let listeners = cloud_provider.listeners().clone();
match cloud_provider.kind() {
CPKind::Aws => match self.kind {
DatabaseKind::Postgresql => {
let db = Box::new(PostgreSQLAws::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger,
));
match (cloud_provider.kind(), &self.kind, &self.mode) {
(CPKind::Aws, DatabaseKind::Postgresql, DatabaseMode::MANAGED) => {
let db = models::database::Database::<AWS, Managed, PostgresSQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
DatabaseKind::Mysql => {
let db = Box::new(MySQLAws::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger,
));
Some(Box::new(db))
}
(CPKind::Aws, DatabaseKind::Postgresql, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<AWS, Container, PostgresSQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
DatabaseKind::Mongodb => {
let db = Box::new(MongoDbAws::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger,
));
Some(Box::new(db))
}
Some(db)
}
DatabaseKind::Redis => {
let db = Box::new(RedisAws::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger,
));
(CPKind::Aws, DatabaseKind::Mysql, DatabaseMode::MANAGED) => {
let db = models::database::Database::<AWS, Managed, MySQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
},
CPKind::Do => match self.kind {
DatabaseKind::Postgresql => {
let db = Box::new(PostgresDo::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger,
));
Some(Box::new(db))
}
(CPKind::Aws, DatabaseKind::Mysql, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<AWS, Container, MySQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
DatabaseKind::Mysql => {
let db = Box::new(MySQLDo::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger,
));
Some(Box::new(db))
}
(CPKind::Aws, DatabaseKind::Redis, DatabaseMode::MANAGED) => {
let db = models::database::Database::<AWS, Managed, Redis>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
DatabaseKind::Redis => {
let db = Box::new(RedisDo::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger,
));
Some(Box::new(db))
}
(CPKind::Aws, DatabaseKind::Redis, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<AWS, Container, Redis>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
DatabaseKind::Mongodb => {
let db = Box::new(MongoDo::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger,
));
Some(Box::new(db))
}
(CPKind::Aws, DatabaseKind::Mongodb, DatabaseMode::MANAGED) => {
let db = models::database::Database::<AWS, Managed, MongoDB>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
},
CPKind::Scw => match self.kind {
DatabaseKind::Postgresql => match VersionsNumber::from_str(self.version.as_str()) {
Ok(v) => {
let db = Box::new(PostgresScw::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
v,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger.clone(),
));
Some(Box::new(db))
}
(CPKind::Aws, DatabaseKind::Mongodb, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<AWS, Container, MongoDB>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
Err(e) => {
error!(
"{}",
format!(
"error while parsing postgres version, error: {}",
e.message(ErrorMessageVerbosity::FullDetails)
)
);
None
}
},
DatabaseKind::Mysql => match VersionsNumber::from_str(self.version.as_str()) {
Ok(v) => {
let db = Box::new(MySQLScw::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
v,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger.clone(),
));
Some(Box::new(db))
}
Some(db)
}
Err(e) => {
error!(
"{}",
format!(
"error while parsing mysql version, error: {}",
e.message(ErrorMessageVerbosity::FullDetails)
)
);
None
}
},
DatabaseKind::Redis => {
let db = Box::new(RedisScw::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger.clone(),
));
(CPKind::Do, DatabaseKind::Postgresql, DatabaseMode::MANAGED) => None,
(CPKind::Do, DatabaseKind::Postgresql, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<DO, Container, PostgresSQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
DatabaseKind::Mongodb => {
let db = Box::new(MongoDbScw::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
self.version.as_str(),
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options,
listeners,
logger,
));
Some(Box::new(db))
}
(CPKind::Do, DatabaseKind::Mysql, DatabaseMode::MANAGED) => None,
(CPKind::Do, DatabaseKind::Mysql, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<DO, Container, MySQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(db)
}
},
Some(Box::new(db))
}
(CPKind::Do, DatabaseKind::Redis, DatabaseMode::MANAGED) => None,
(CPKind::Do, DatabaseKind::Redis, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<DO, Container, Redis>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(Box::new(db))
}
(CPKind::Do, DatabaseKind::Mongodb, DatabaseMode::MANAGED) => None,
(CPKind::Do, DatabaseKind::Mongodb, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<DO, Container, MongoDB>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(Box::new(db))
}
(CPKind::Scw, DatabaseKind::Postgresql, DatabaseMode::MANAGED) => {
let db = models::database::Database::<SCW, Managed, PostgresSQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(Box::new(db))
}
(CPKind::Scw, DatabaseKind::Postgresql, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<SCW, Container, PostgresSQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(Box::new(db))
}
(CPKind::Scw, DatabaseKind::Mysql, DatabaseMode::MANAGED) => {
let db = models::database::Database::<SCW, Managed, MySQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(Box::new(db))
}
(CPKind::Scw, DatabaseKind::Mysql, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<SCW, Container, MySQL>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(Box::new(db))
}
(CPKind::Scw, DatabaseKind::Redis, DatabaseMode::MANAGED) => {
// Not Implemented
None
}
(CPKind::Scw, DatabaseKind::Redis, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<SCW, Container, Redis>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(Box::new(db))
}
(CPKind::Scw, DatabaseKind::Mongodb, DatabaseMode::MANAGED) => {
// Not Implemented
None
}
(CPKind::Scw, DatabaseKind::Mongodb, DatabaseMode::CONTAINER) => {
let db = models::database::Database::<SCW, Container, MongoDB>::new(
context.clone(),
self.id.as_str(),
self.action.to_service_action(),
self.name.as_str(),
VersionsNumber::from_str(self.version.as_str()).ok()?,
self.fqdn.as_str(),
self.fqdn_id.as_str(),
self.total_cpus.clone(),
self.total_ram_in_mib,
self.database_instance_type.as_str(),
database_options.publicly_accessible,
database_options.port,
database_options,
listeners,
logger,
)
.unwrap();
Some(Box::new(db))
}
}
}
}

View File

@@ -26,23 +26,23 @@ pub enum ApplicationError {
pub struct Application<T: CloudProvider> {
_marker: PhantomData<T>,
pub(crate) context: Context,
pub(crate) id: String,
pub(crate) action: Action,
pub(crate) name: String,
pub(crate) ports: Vec<Port>,
pub(crate) total_cpus: String,
pub(crate) cpu_burst: String,
pub(crate) total_ram_in_mib: u32,
pub(crate) min_instances: u32,
pub(crate) max_instances: u32,
pub(crate) start_timeout_in_seconds: u32,
pub(crate) build: Build,
pub(crate) storage: Vec<Storage<T::StorageTypes>>,
pub(crate) environment_variables: Vec<EnvironmentVariable>,
pub(crate) listeners: Listeners,
pub(crate) logger: Box<dyn Logger>,
pub(crate) _extra_settings: T::AppExtraSettings,
pub(super) context: Context,
pub(super) id: String,
pub(super) action: Action,
pub(super) name: String,
pub(super) ports: Vec<Port>,
pub(super) total_cpus: String,
pub(super) cpu_burst: String,
pub(super) total_ram_in_mib: u32,
pub(super) min_instances: u32,
pub(super) max_instances: u32,
pub(super) start_timeout_in_seconds: u32,
pub(super) build: Build,
pub(super) storage: Vec<Storage<T::StorageTypes>>,
pub(super) environment_variables: Vec<EnvironmentVariable>,
pub(super) listeners: Listeners,
pub(super) logger: Box<dyn Logger>,
pub(super) _extra_settings: T::AppExtraSettings,
}
// Here we define the common behavior among all providers
@@ -294,7 +294,7 @@ impl<T: CloudProvider> Helm for Application<T> {
format!(
"{}/{}/charts/q-application",
self.context.lib_root_dir(),
T::helm_directory_name(),
T::lib_directory_name(),
)
}

326
src/models/aws/database.rs Normal file
View File

@@ -0,0 +1,326 @@
use crate::cloud_provider::service::{
check_service_version, default_tera_context, get_tfstate_name, get_tfstate_suffix, DatabaseOptions, Service,
ServiceVersionCheckResult,
};
use crate::cloud_provider::{service, DeploymentTarget};
use crate::cmd::kubectl;
use crate::errors::EngineError;
use crate::events::{EnvironmentStep, EventDetails, Stage};
use crate::models::aws::database_utils::{
get_managed_mongodb_version, get_managed_mysql_version, get_managed_postgres_version, get_managed_redis_version,
};
use crate::models::database::{
Container, Database, DatabaseMode, DatabaseType, Managed, MongoDB, MySQL, PostgresSQL, Redis,
};
use crate::models::types::{ToTeraContext, AWS};
use tera::Context as TeraContext;
/////////////////////////////////////////////////////////////////
// CONTAINER
impl DatabaseType<AWS, Container> for PostgresSQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"PostgresSQL"
}
fn lib_directory_name() -> &'static str {
"postgresql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::PostgreSQL
}
}
impl DatabaseType<AWS, Container> for MySQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"MySQL"
}
fn lib_directory_name() -> &'static str {
"mysql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::MySQL
}
}
impl DatabaseType<AWS, Container> for Redis {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"Redis"
}
fn lib_directory_name() -> &'static str {
"redis"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::Redis
}
}
impl DatabaseType<AWS, Container> for MongoDB {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"Redis"
}
fn lib_directory_name() -> &'static str {
"mongodb"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::MongoDB
}
}
/////////////////////////////////////////////////////////////////
// MANAGED
impl DatabaseType<AWS, Managed> for PostgresSQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"Postgres RDS"
}
fn lib_directory_name() -> &'static str {
"postgresql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::PostgreSQL
}
}
impl DatabaseType<AWS, Managed> for MySQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"MySQL RDS"
}
fn lib_directory_name() -> &'static str {
"mysql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::MySQL
}
}
impl DatabaseType<AWS, Managed> for Redis {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"ElasticCache"
}
fn lib_directory_name() -> &'static str {
"redis"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::Redis
}
}
impl DatabaseType<AWS, Managed> for MongoDB {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"DocumentDB"
}
fn lib_directory_name() -> &'static str {
"mongodb"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::MongoDB
}
}
impl<T: DatabaseType<AWS, Managed>> Database<AWS, Managed, T>
where
Database<AWS, Managed, T>: Service,
{
fn get_version_aws_managed(&self, event_details: EventDetails) -> Result<ServiceVersionCheckResult, EngineError> {
let fn_version = match T::db_type() {
service::DatabaseType::PostgreSQL => get_managed_postgres_version,
service::DatabaseType::MongoDB => get_managed_mongodb_version,
service::DatabaseType::MySQL => get_managed_mysql_version,
service::DatabaseType::Redis => get_managed_redis_version,
};
check_service_version(fn_version(self.version.to_string()), self, event_details, self.logger())
}
fn to_tera_context_for_aws_managed(
&self,
target: &DeploymentTarget,
options: &DatabaseOptions,
) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = self
.get_version_aws_managed(event_details)?
.matched_version()
.to_string();
context.insert("version", &version);
// Specific to mysql
if T::db_type() == service::DatabaseType::MySQL {
context.insert(
"parameter_group_family",
&format!(
"mysql{}.{}",
self.version.major,
self.version.minor.as_deref().unwrap_or_default()
),
);
}
// Specific for redis
if T::db_type() == service::DatabaseType::Redis {
let parameter_group_name = if self.version.major == "5" {
"default.redis5.0"
} else if self.version.major == "6" {
"default.redis6.x"
} else {
"redis.unknown"
};
context.insert("database_elasticache_parameter_group_name", parameter_group_name);
}
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, Managed::is_managed()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_name", self.sanitized_name().as_str());
context.insert("database_db_name", self.name());
context.insert("database_login", options.login.as_str());
context.insert("database_password", options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &options.database_disk_type);
context.insert("encrypt_disk", &options.encrypt_disk);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("skip_final_snapshot", &false);
context.insert("final_snapshot_name", &format!("qovery-{}-final-snap", self.id));
context.insert("delete_automated_backups", &self.context().is_test_cluster());
context.insert("publicly_accessible", &options.publicly_accessible);
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
}
////////////////////////////////////////////////////////////////////////:
// POSTGRES SQL
impl ToTeraContext for Database<AWS, Managed, PostgresSQL>
where
PostgresSQL: DatabaseType<AWS, Managed>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
self.to_tera_context_for_aws_managed(target, &self.options)
}
}
impl ToTeraContext for Database<AWS, Container, PostgresSQL>
where
PostgresSQL: DatabaseType<AWS, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
self.to_tera_context_for_container(target, &self.options)
}
}
////////////////////////////////////////////////////////////////////////:
// MySQL
impl ToTeraContext for Database<AWS, Managed, MySQL>
where
MySQL: DatabaseType<AWS, Managed>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
self.to_tera_context_for_aws_managed(target, &self.options)
}
}
impl ToTeraContext for Database<AWS, Container, MySQL>
where
MySQL: DatabaseType<AWS, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
self.to_tera_context_for_container(target, &self.options)
}
}
////////////////////////////////////////////////////////////////////////:
// MongoDB
impl ToTeraContext for Database<AWS, Managed, MongoDB>
where
MongoDB: DatabaseType<AWS, Managed>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
self.to_tera_context_for_aws_managed(target, &self.options)
}
}
impl ToTeraContext for Database<AWS, Container, MongoDB>
where
MongoDB: DatabaseType<AWS, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
self.to_tera_context_for_container(target, &self.options)
}
}
////////////////////////////////////////////////////////////////////////:
// Redis
impl ToTeraContext for Database<AWS, Managed, Redis>
where
Redis: DatabaseType<AWS, Managed>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
self.to_tera_context_for_aws_managed(target, &self.options)
}
}
impl ToTeraContext for Database<AWS, Container, Redis>
where
Redis: DatabaseType<AWS, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
self.to_tera_context_for_container(target, &self.options)
}
}

View File

@@ -0,0 +1,198 @@
use crate::errors::CommandError;
use crate::models::database_utils::{generate_supported_version, get_supported_version_to_use};
use std::collections::HashMap;
pub(super) fn get_managed_mysql_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_mysql_versions = HashMap::new();
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_MySQL.html#MySQL.Concepts.VersionMgmt
// v5.7
let mut v57 = generate_supported_version(5, 7, 7, Some(16), Some(34), None);
v57.remove("5.7.32");
v57.remove("5.7.29");
v57.remove("5.7.27");
v57.remove("5.7.20");
v57.remove("5.7.18");
supported_mysql_versions.extend(v57);
// v8
let mut v8 = generate_supported_version(8, 0, 0, Some(11), Some(26), None);
v8.remove("8.0.24");
v8.remove("8.0.22");
v8.remove("8.0.18");
v8.remove("8.0.14");
v8.remove("8.0.12");
supported_mysql_versions.extend(v8);
get_supported_version_to_use("RDS MySQL", supported_mysql_versions, requested_version)
}
pub(super) fn get_managed_mongodb_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_mongodb_versions = HashMap::new();
// v3.6.0
let mongo_version = generate_supported_version(3, 6, 6, Some(0), Some(0), None);
supported_mongodb_versions.extend(mongo_version);
// v4.0.0
let mongo_version = generate_supported_version(4, 0, 0, Some(0), Some(0), None);
supported_mongodb_versions.extend(mongo_version);
get_supported_version_to_use("DocumentDB", supported_mongodb_versions, requested_version)
}
pub(super) fn get_managed_postgres_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_postgres_versions = HashMap::new();
// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html#PostgreSQL.Concepts
// v10
let mut v10 = generate_supported_version(10, 1, 18, None, None, None);
v10.remove("10.2"); // non supported version by AWS
v10.remove("10.8"); // non supported version by AWS
supported_postgres_versions.extend(v10);
// v11
let mut v11 = generate_supported_version(11, 1, 13, None, None, None);
v11.remove("11.3"); // non supported version by AWS
supported_postgres_versions.extend(v11);
// v12
let v12 = generate_supported_version(12, 2, 8, None, None, None);
supported_postgres_versions.extend(v12);
// v13
let v13 = generate_supported_version(13, 1, 4, None, None, None);
supported_postgres_versions.extend(v13);
get_supported_version_to_use("Postgresql", supported_postgres_versions, requested_version)
}
pub(super) fn get_managed_redis_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_redis_versions = HashMap::with_capacity(2);
// https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/supported-engine-versions.html
supported_redis_versions.insert("6".to_string(), "6.x".to_string());
supported_redis_versions.insert("5".to_string(), "5.0.6".to_string());
get_supported_version_to_use("Elasticache", supported_redis_versions, requested_version)
}
#[cfg(test)]
mod tests {
use crate::errors::ErrorMessageVerbosity::SafeOnly;
use crate::models::aws::database_utils::{
get_managed_mongodb_version, get_managed_mysql_version, get_managed_postgres_version, get_managed_redis_version,
};
use crate::models::database_utils::{
get_self_hosted_mongodb_version, get_self_hosted_mysql_version, get_self_hosted_postgres_version,
get_self_hosted_redis_version,
};
#[test]
fn check_postgres_version() {
// managed version
assert_eq!(get_managed_postgres_version("12".to_string()).unwrap(), "12.8");
assert_eq!(get_managed_postgres_version("12.3".to_string()).unwrap(), "12.3");
assert_eq!(
get_managed_postgres_version("12.3.0".to_string())
.unwrap_err()
.message(SafeOnly)
.as_str(),
"Postgresql 12.3.0 version is not supported"
);
assert_eq!(
get_managed_postgres_version("11.3".to_string())
.unwrap_err()
.message(SafeOnly)
.as_str(),
"Postgresql 11.3 version is not supported"
);
// self-hosted version
assert_eq!(get_self_hosted_postgres_version("12".to_string()).unwrap(), "12.8.0");
assert_eq!(get_self_hosted_postgres_version("12.8".to_string()).unwrap(), "12.8.0");
assert_eq!(get_self_hosted_postgres_version("12.3.0".to_string()).unwrap(), "12.3.0");
assert_eq!(
get_self_hosted_postgres_version("1.0".to_string())
.unwrap_err()
.message(SafeOnly)
.as_str(),
"Postgresql 1.0 version is not supported"
);
}
#[test]
fn check_redis_version() {
// managed version
assert_eq!(get_managed_redis_version("6".to_string()).unwrap(), "6.x");
assert_eq!(get_managed_redis_version("5".to_string()).unwrap(), "5.0.6");
assert_eq!(
get_managed_redis_version("1.0".to_string())
.unwrap_err()
.message(SafeOnly)
.as_str(),
"Elasticache 1.0 version is not supported"
);
// self-hosted version
assert_eq!(get_self_hosted_redis_version("6".to_string()).unwrap(), "6.0.9");
assert_eq!(get_self_hosted_redis_version("6.0".to_string()).unwrap(), "6.0.9");
assert_eq!(
get_self_hosted_redis_version("1.0".to_string())
.unwrap_err()
.message(SafeOnly)
.as_str(),
"Redis 1.0 version is not supported"
);
}
#[test]
fn check_mysql_version() {
// managed version
assert_eq!(get_managed_mysql_version("8".to_string()).unwrap(), "8.0.26");
assert_eq!(get_managed_mysql_version("8.0".to_string()).unwrap(), "8.0.26");
assert_eq!(get_managed_mysql_version("8.0.16".to_string()).unwrap(), "8.0.16");
assert_eq!(
get_managed_mysql_version("8.0.18".to_string())
.unwrap_err()
.message(SafeOnly)
.as_str(),
"RDS MySQL 8.0.18 version is not supported"
);
// self-hosted version
assert_eq!(get_self_hosted_mysql_version("5".to_string()).unwrap(), "5.7.34");
assert_eq!(get_self_hosted_mysql_version("5.7".to_string()).unwrap(), "5.7.34");
assert_eq!(get_self_hosted_mysql_version("5.7.31".to_string()).unwrap(), "5.7.31");
assert_eq!(
get_self_hosted_mysql_version("1.0".to_string())
.unwrap_err()
.message(SafeOnly)
.as_str(),
"MySQL 1.0 version is not supported"
);
}
#[test]
fn check_mongodb_version() {
// managed version
assert_eq!(get_managed_mongodb_version("4".to_string()).unwrap(), "4.0.0");
assert_eq!(get_managed_mongodb_version("4.0".to_string()).unwrap(), "4.0.0");
assert_eq!(
get_managed_mongodb_version("4.4".to_string())
.unwrap_err()
.message(SafeOnly)
.as_str(),
"DocumentDB 4.4 version is not supported"
);
// self-hosted version
assert_eq!(get_self_hosted_mongodb_version("4".to_string()).unwrap(), "4.4.4");
assert_eq!(get_self_hosted_mongodb_version("4.2".to_string()).unwrap(), "4.2.12");
assert_eq!(
get_self_hosted_mongodb_version("3.4".to_string())
.unwrap_err()
.message(SafeOnly)
.as_str(),
"MongoDB 3.4 version is not supported"
);
}
}

View File

@@ -1,5 +1,7 @@
pub mod application;
pub mod router;
mod application;
mod database;
mod database_utils;
mod router;
use crate::models::types::CloudProvider;
use crate::models::types::AWS;
@@ -30,7 +32,7 @@ impl CloudProvider for AWS {
"Elastic Container Registry"
}
fn helm_directory_name() -> &'static str {
fn lib_directory_name() -> &'static str {
"aws"
}
}

508
src/models/database.rs Normal file
View File

@@ -0,0 +1,508 @@
use crate::cloud_provider::service::{
check_service_version, default_tera_context, delete_stateful_service, deploy_stateful_service, get_tfstate_name,
get_tfstate_suffix, scale_down_database, send_progress_on_long_task, Action, Create, DatabaseOptions, Delete, Helm,
Pause, Service, ServiceType, ServiceVersionCheckResult, StatefulService, Terraform,
};
use crate::cloud_provider::utilities::{check_domain_for, managed_db_name_sanitizer, print_action};
use crate::cloud_provider::{service, DeploymentTarget};
use crate::cmd::helm::Timeout;
use crate::cmd::kubectl;
use crate::errors::EngineError;
use crate::events::{EnvironmentStep, EventDetails, Stage, ToTransmitter, Transmitter};
use crate::io_models::{Context, Listen, Listener, Listeners, ListenersHelper};
use crate::logger::Logger;
use crate::models::database_utils::{
get_self_hosted_mongodb_version, get_self_hosted_mysql_version, get_self_hosted_postgres_version,
get_self_hosted_redis_version,
};
use crate::models::types::{CloudProvider, ToTeraContext, VersionsNumber};
use function_name::named;
use std::borrow::Borrow;
use std::marker::PhantomData;
use tera::Context as TeraContext;
/////////////////////////////////////////////////////////////////
// Database mode
pub struct Managed {}
pub struct Container {}
pub trait DatabaseMode {
fn is_managed() -> bool;
fn is_container() -> bool {
!Self::is_managed()
}
}
impl DatabaseMode for Managed {
fn is_managed() -> bool {
true
}
}
impl DatabaseMode for Container {
fn is_managed() -> bool {
false
}
}
/////////////////////////////////////////////////////////////////
// Database types, will be only used as a marker
pub struct PostgresSQL {}
pub struct MySQL {}
pub struct MongoDB {}
pub struct Redis {}
pub trait DatabaseType<T: CloudProvider, M: DatabaseMode> {
type DatabaseOptions;
fn short_name() -> &'static str;
fn lib_directory_name() -> &'static str;
fn db_type() -> service::DatabaseType;
}
#[derive(thiserror::Error, Debug)]
pub enum DatabaseError {
#[error("Application invalid configuration: {0}")]
InvalidConfig(String),
}
pub struct Database<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> {
_marker: PhantomData<(C, M, T)>,
pub(super) context: Context,
pub(super) id: String,
pub(super) action: Action,
pub(super) name: String,
pub(super) version: VersionsNumber,
pub(super) fqdn: String,
pub(super) fqdn_id: String,
pub(super) total_cpus: String,
pub(super) total_ram_in_mib: u32,
pub(super) database_instance_type: String,
pub(super) publicly_accessible: bool,
pub(super) private_port: u16,
pub(super) options: T::DatabaseOptions,
pub(super) listeners: Listeners,
pub(super) logger: Box<dyn Logger>,
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> Database<C, M, T> {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
version: VersionsNumber,
fqdn: &str,
fqdn_id: &str,
total_cpus: String,
total_ram_in_mib: u32,
database_instance_type: &str,
publicly_accessible: bool,
private_port: u16,
options: T::DatabaseOptions,
listeners: Listeners,
logger: Box<dyn Logger>,
) -> Result<Self, DatabaseError> {
// TODO: Implement domain constraint logic
Ok(Self {
_marker: PhantomData,
context,
action,
id: id.to_string(),
name: name.to_string(),
version,
fqdn: fqdn.to_string(),
fqdn_id: fqdn_id.to_string(),
total_cpus,
total_ram_in_mib,
database_instance_type: database_instance_type.to_string(),
publicly_accessible,
private_port,
options,
listeners,
logger,
})
}
fn selector(&self) -> String {
format!("databaseId={}", self.id)
}
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> Terraform for Database<C, M, T> {
fn terraform_common_resource_dir_path(&self) -> String {
format!("{}/{}/services/common", self.context.lib_root_dir(), C::lib_directory_name())
}
fn terraform_resource_dir_path(&self) -> String {
format!(
"{}/{}/services/{}",
self.context.lib_root_dir(),
C::lib_directory_name(),
T::lib_directory_name()
)
}
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> Listen for Database<C, M, T> {
fn listeners(&self) -> &Listeners {
&self.listeners
}
fn add_listener(&mut self, listener: Listener) {
self.listeners.push(listener);
}
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> ToTransmitter for Database<C, M, T> {
fn to_transmitter(&self) -> Transmitter {
Transmitter::Database(self.id.to_string(), T::short_name().to_string(), self.name.to_string())
}
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> Service for Database<C, M, T>
where
Database<C, M, T>: ToTeraContext,
{
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Database(T::db_type())
}
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.name
}
fn sanitized_name(&self) -> String {
// FIXME: specific case only for aws ;'(
// This is sad, but can't change that as it would break/wipe all container db for users
if C::lib_directory_name() == "aws" {
managed_db_name_sanitizer(60, T::lib_directory_name(), &self.id)
} else {
format!("{}-{}", T::lib_directory_name(), &self.id)
}
}
fn version(&self) -> String {
self.version.to_string()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
Some(self.private_port)
}
fn start_timeout(&self) -> Timeout<u32> {
Timeout::Default
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn cpu_burst(&self) -> String {
self.total_cpus.to_string()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn min_instances(&self) -> u32 {
1
}
fn max_instances(&self) -> u32 {
1
}
fn publicly_accessible(&self) -> bool {
self.publicly_accessible
}
fn tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
self.to_tera_context(target)
}
fn logger(&self) -> &dyn Logger {
self.logger.borrow()
}
fn selector(&self) -> Option<String> {
Some(self.selector())
}
}
impl<Cloud: CloudProvider, M: DatabaseMode, DbType: DatabaseType<Cloud, M>> Helm for Database<Cloud, M, DbType> {
fn helm_selector(&self) -> Option<String> {
Some(self.selector())
}
fn helm_release_name(&self) -> String {
format!("{}-{}", DbType::lib_directory_name(), self.id)
}
fn helm_chart_dir(&self) -> String {
format!(
"{}/common/services/{}",
self.context.lib_root_dir(),
DbType::lib_directory_name()
)
}
fn helm_chart_values_dir(&self) -> String {
format!(
"{}/{}/chart_values/{}",
self.context.lib_root_dir(),
Cloud::lib_directory_name(),
DbType::lib_directory_name()
)
}
fn helm_chart_external_name_service_dir(&self) -> String {
format!("{}/common/charts/external-name-svc", self.context.lib_root_dir())
}
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> Create for Database<C, M, T>
where
Database<C, M, T>: ToTeraContext,
{
#[named]
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
C::short_name(),
T::db_type().to_string().as_str(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, Action::Create, || {
deploy_stateful_service(target, self, event_details.clone(), self.logger())
})
}
#[named]
fn on_create_check(&self) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
C::short_name(),
T::db_type().to_string().as_str(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
if self.publicly_accessible {
check_domain_for(
ListenersHelper::new(&self.listeners),
vec![&self.fqdn],
self.context.execution_id(),
self.context.execution_id(),
event_details,
self.logger(),
)?;
}
Ok(())
}
#[named]
fn on_create_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Deploy));
print_action(
C::short_name(),
T::db_type().to_string().as_str(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> Pause for Database<C, M, T>
where
Database<C, M, T>: ToTeraContext,
{
#[named]
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
C::short_name(),
T::db_type().to_string().as_str(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
send_progress_on_long_task(self, Action::Pause, || scale_down_database(target, self, 0))
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_pause_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Pause));
print_action(
C::short_name(),
T::db_type().to_string().as_str(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> Delete for Database<C, M, T>
where
Database<C, M, T>: ToTeraContext,
{
#[named]
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
C::short_name(),
T::db_type().to_string().as_str(),
function_name!(),
self.name(),
event_details.clone(),
self.logger(),
);
send_progress_on_long_task(self, Action::Delete, || {
delete_stateful_service(target, self, event_details.clone(), self.logger())
})
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
#[named]
fn on_delete_error(&self, _target: &DeploymentTarget) -> Result<(), EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::Delete));
print_action(
C::short_name(),
T::db_type().to_string().as_str(),
function_name!(),
self.name(),
event_details,
self.logger(),
);
Ok(())
}
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> StatefulService for Database<C, M, T>
where
Database<C, M, T>: ToTeraContext,
{
fn as_stateful_service(&self) -> &dyn StatefulService {
self
}
fn is_managed_service(&self) -> bool {
M::is_managed()
}
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> service::Database for Database<C, M, T> where
Database<C, M, T>: ToTeraContext
{
}
impl<C: CloudProvider, M: DatabaseMode, T: DatabaseType<C, M>> Database<C, M, T>
where
Database<C, M, T>: Service,
{
fn get_version(&self, event_details: EventDetails) -> Result<ServiceVersionCheckResult, EngineError> {
let fn_version = match T::db_type() {
service::DatabaseType::PostgreSQL => get_self_hosted_postgres_version,
service::DatabaseType::MongoDB => get_self_hosted_mongodb_version,
service::DatabaseType::MySQL => get_self_hosted_mysql_version,
service::DatabaseType::Redis => get_self_hosted_redis_version,
};
check_service_version(fn_version(self.version.to_string()), self, event_details, self.logger())
}
pub(super) fn to_tera_context_for_container(
&self,
target: &DeploymentTarget,
options: &DatabaseOptions,
) -> Result<TeraContext, EngineError> {
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = self.get_version(event_details)?.matched_version().to_string();
context.insert("version", &version);
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, M::is_managed()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_db_name", self.name());
context.insert("database_login", options.login.as_str());
context.insert("database_password", options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &self.publicly_accessible);
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
}

View File

@@ -0,0 +1,199 @@
use crate::errors::CommandError;
use crate::models::types::VersionsNumber;
use std::collections::HashMap;
use std::str::FromStr;
pub fn get_self_hosted_postgres_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_postgres_versions = HashMap::new();
// https://hub.docker.com/r/bitnami/postgresql/tags?page=1&ordering=last_updated
// v10
let v10 = generate_supported_version(10, 1, 16, Some(0), Some(0), None);
supported_postgres_versions.extend(v10);
// v11
let v11 = generate_supported_version(11, 1, 11, Some(0), Some(0), None);
supported_postgres_versions.extend(v11);
// v12
let v12 = generate_supported_version(12, 2, 8, Some(0), Some(0), None);
supported_postgres_versions.extend(v12);
// v13
let v13 = generate_supported_version(13, 1, 4, Some(0), Some(0), None);
supported_postgres_versions.extend(v13);
get_supported_version_to_use("Postgresql", supported_postgres_versions, requested_version)
}
pub fn get_self_hosted_mysql_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_mysql_versions = HashMap::new();
// https://hub.docker.com/r/bitnami/mysql/tags?page=1&ordering=last_updated
// v5.7
let v57 = generate_supported_version(5, 7, 7, Some(16), Some(34), None);
supported_mysql_versions.extend(v57);
// v8
let v8 = generate_supported_version(8, 0, 0, Some(11), Some(24), None);
supported_mysql_versions.extend(v8);
get_supported_version_to_use("MySQL", supported_mysql_versions, requested_version)
}
pub fn get_self_hosted_mongodb_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_mongodb_versions = HashMap::new();
// https://hub.docker.com/r/bitnami/mongodb/tags?page=1&ordering=last_updated
// v3.6
let mongo_version = generate_supported_version(3, 6, 6, Some(0), Some(22), None);
supported_mongodb_versions.extend(mongo_version);
// v4.0
let mongo_version = generate_supported_version(4, 0, 0, Some(0), Some(23), None);
supported_mongodb_versions.extend(mongo_version);
// v4.2
let mongo_version = generate_supported_version(4, 2, 2, Some(0), Some(12), None);
supported_mongodb_versions.extend(mongo_version);
// v4.4
let mongo_version = generate_supported_version(4, 4, 4, Some(0), Some(4), None);
supported_mongodb_versions.extend(mongo_version);
get_supported_version_to_use("MongoDB", supported_mongodb_versions, requested_version)
}
pub fn get_self_hosted_redis_version(requested_version: String) -> Result<String, CommandError> {
let mut supported_redis_versions = HashMap::with_capacity(4);
// https://hub.docker.com/r/bitnami/redis/tags?page=1&ordering=last_updated
supported_redis_versions.insert("6".to_string(), "6.0.9".to_string());
supported_redis_versions.insert("6.0".to_string(), "6.0.9".to_string());
supported_redis_versions.insert("5".to_string(), "5.0.10".to_string());
supported_redis_versions.insert("5.0".to_string(), "5.0.10".to_string());
get_supported_version_to_use("Redis", supported_redis_versions, requested_version)
}
pub fn get_supported_version_to_use(
database_name: &str,
all_supported_versions: HashMap<String, String>,
version_to_check: String,
) -> Result<String, CommandError> {
let version = VersionsNumber::from_str(version_to_check.as_str())?;
// if a patch version is required
if version.patch.is_some() {
return match all_supported_versions.get(&format!(
"{}.{}.{}",
version.major,
version.minor.unwrap(),
version.patch.unwrap()
)) {
Some(version) => Ok(version.to_string()),
None => {
return Err(CommandError::new_from_safe_message(format!(
"{} {} version is not supported",
database_name, version_to_check
)));
}
};
}
// if a minor version is required
if version.minor.is_some() {
return match all_supported_versions.get(&format!("{}.{}", version.major, version.minor.unwrap())) {
Some(version) => Ok(version.to_string()),
None => {
return Err(CommandError::new_from_safe_message(format!(
"{} {} version is not supported",
database_name, version_to_check
)));
}
};
};
// if only a major version is required
match all_supported_versions.get(&version.major) {
Some(version) => Ok(version.to_string()),
None => {
return Err(CommandError::new_from_safe_message(format!(
"{} {} version is not supported",
database_name, version_to_check
)));
}
}
}
// Ease the support of multiple versions by range
pub fn generate_supported_version(
major: i32,
minor_min: i32,
minor_max: i32,
update_min: Option<i32>,
update_max: Option<i32>,
suffix_version: Option<String>,
) -> HashMap<String, String> {
let mut supported_versions = HashMap::new();
let latest_major_version;
// blank suffix if not requested
let suffix = match suffix_version {
Some(suffix) => suffix,
None => "".to_string(),
};
let _ = match update_min {
// manage minor with updates
Some(_) => {
latest_major_version = format!("{}.{}.{}{}", major, minor_max, update_max.unwrap(), suffix);
if minor_min == minor_max {
// add short minor format targeting latest version
supported_versions.insert(format!("{}.{}", major, minor_max), latest_major_version.clone());
if update_min.unwrap() == update_max.unwrap() {
let version = format!("{}.{}.{}", major, minor_min, update_min.unwrap());
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
} else {
for update in update_min.unwrap()..update_max.unwrap() + 1 {
let version = format!("{}.{}.{}", major, minor_min, update);
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
}
}
} else {
for minor in minor_min..minor_max + 1 {
// add short minor format targeting latest version
supported_versions.insert(
format!("{}.{}", major, minor),
format!("{}.{}.{}", major, minor, update_max.unwrap()),
);
if update_min.unwrap() == update_max.unwrap() {
let version = format!("{}.{}.{}", major, minor, update_min.unwrap());
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
} else {
for update in update_min.unwrap()..update_max.unwrap() + 1 {
let version = format!("{}.{}.{}", major, minor, update);
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
}
}
}
}
}
// manage minor without updates
None => {
latest_major_version = format!("{}.{}{}", major, minor_max, suffix);
for minor in minor_min..minor_max + 1 {
let version = format!("{}.{}", major, minor);
supported_versions.insert(version.clone(), format!("{}{}", version, suffix));
}
}
};
// default major + major.minor supported version
supported_versions.insert(major.to_string(), latest_major_version);
supported_versions
}

View File

@@ -0,0 +1,152 @@
use crate::cloud_provider::service::{check_service_version, DatabaseOptions, Service};
use crate::cloud_provider::{service, DeploymentTarget};
use crate::errors::EngineError;
use crate::models::database::{Container, Database, DatabaseType, MongoDB, MySQL, PostgresSQL, Redis};
use crate::models::database_utils::{
get_self_hosted_mongodb_version, get_self_hosted_mysql_version, get_self_hosted_postgres_version,
get_self_hosted_redis_version,
};
use crate::models::types::{ToTeraContext, DO};
use tera::Context as TeraContext;
/////////////////////////////////////////////////////////////////
// CONTAINER
impl DatabaseType<DO, Container> for PostgresSQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"PostgresSQL"
}
fn lib_directory_name() -> &'static str {
"postgresql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::PostgreSQL
}
}
impl DatabaseType<DO, Container> for MySQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"MySQL"
}
fn lib_directory_name() -> &'static str {
"mysql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::MySQL
}
}
impl DatabaseType<DO, Container> for Redis {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"Redis"
}
fn lib_directory_name() -> &'static str {
"redis"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::Redis
}
}
impl DatabaseType<DO, Container> for MongoDB {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"Redis"
}
fn lib_directory_name() -> &'static str {
"mongodb"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::MongoDB
}
}
/////////////////////////////////////////////////////////////////
// MANAGED
// DO don't support managed databases for now
////////////////////////////////////////////////////////////////////////:
// POSTGRES SQL
impl ToTeraContext for Database<DO, Container, PostgresSQL>
where
PostgresSQL: DatabaseType<DO, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let _check_version = |event_details| {
check_service_version(
get_self_hosted_postgres_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_container(target, &self.options)
}
}
////////////////////////////////////////////////////////////////////////:
// MySQL
impl ToTeraContext for Database<DO, Container, MySQL>
where
MySQL: DatabaseType<DO, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let _check_version = |event_details| {
check_service_version(
get_self_hosted_mysql_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_container(target, &self.options)
}
}
////////////////////////////////////////////////////////////////////////:
// MongoDB
impl ToTeraContext for Database<DO, Container, MongoDB>
where
MongoDB: DatabaseType<DO, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let _check_version = |event_details| {
check_service_version(
get_self_hosted_mongodb_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_container(target, &self.options)
}
}
////////////////////////////////////////////////////////////////////////:
// Redis
impl ToTeraContext for Database<DO, Container, Redis>
where
Redis: DatabaseType<DO, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let _check_version = |event_details| {
check_service_version(
get_self_hosted_redis_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_container(target, &self.options)
}
}

View File

@@ -1,4 +1,5 @@
mod application;
mod database;
mod router;
use crate::errors::CommandError;
@@ -34,7 +35,7 @@ impl CloudProvider for DO {
"Digital Ocean Container Registry"
}
fn helm_directory_name() -> &'static str {
fn lib_directory_name() -> &'static str {
"digitalocean"
}
}

View File

@@ -1,5 +1,7 @@
pub mod application;
pub mod aws;
pub mod database;
pub(crate) mod database_utils;
pub mod digital_ocean;
pub mod router;
pub mod scaleway;

View File

@@ -218,7 +218,7 @@ impl<T: CloudProvider> Helm for Router<T> {
format!(
"{}/{}/chart_values/nginx-ingress",
self.context.lib_root_dir(),
T::helm_directory_name()
T::lib_directory_name()
)
}
@@ -333,7 +333,7 @@ where
let from_dir = format!(
"{}/{}/charts/q-ingress-tls",
self.context.lib_root_dir(),
T::helm_directory_name()
T::lib_directory_name()
);
if let Err(e) =
crate::template::generate_and_copy_all_files_into_dir(from_dir.as_str(), workspace_dir.as_str(), context)

View File

@@ -0,0 +1,293 @@
use crate::cloud_provider::service::{
check_service_version, default_tera_context, get_tfstate_name, get_tfstate_suffix, DatabaseOptions, Service,
ServiceVersionCheckResult,
};
use crate::cloud_provider::{service, DeploymentTarget};
use crate::cmd::kubectl;
use crate::errors::EngineError;
use crate::events::{EnvironmentStep, EventDetails, Stage};
use crate::models::database::{
Container, Database, DatabaseMode, DatabaseType, Managed, MongoDB, MySQL, PostgresSQL, Redis,
};
use crate::models::database_utils::{
get_self_hosted_mongodb_version, get_self_hosted_mysql_version, get_self_hosted_postgres_version,
get_self_hosted_redis_version,
};
use crate::models::scaleway::database_utils::{pick_managed_mysql_version, pick_managed_postgres_version};
use crate::models::types::{ToTeraContext, SCW};
use tera::Context as TeraContext;
/////////////////////////////////////////////////////////////////
// CONTAINER
impl DatabaseType<SCW, Container> for PostgresSQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"PostgresSQL"
}
fn lib_directory_name() -> &'static str {
"postgresql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::PostgreSQL
}
}
impl DatabaseType<SCW, Container> for MySQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"MySQL"
}
fn lib_directory_name() -> &'static str {
"mysql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::MySQL
}
}
impl DatabaseType<SCW, Container> for Redis {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"Redis"
}
fn lib_directory_name() -> &'static str {
"redis"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::Redis
}
}
impl DatabaseType<SCW, Container> for MongoDB {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"Redis"
}
fn lib_directory_name() -> &'static str {
"mongodb"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::MongoDB
}
}
/////////////////////////////////////////////////////////////////
// MANAGED
impl DatabaseType<SCW, Managed> for PostgresSQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"Postgres Managed"
}
fn lib_directory_name() -> &'static str {
"postgresql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::PostgreSQL
}
}
impl DatabaseType<SCW, Managed> for MySQL {
type DatabaseOptions = DatabaseOptions;
fn short_name() -> &'static str {
"MySQL Managed"
}
fn lib_directory_name() -> &'static str {
"mysql"
}
fn db_type() -> service::DatabaseType {
service::DatabaseType::MySQL
}
}
// Redis and MongoDB are not supported managed db yet
impl<M: DatabaseMode, T: DatabaseType<SCW, M>> Database<SCW, M, T> {
fn to_tera_context_for_scaleway_managed(
&self,
target: &DeploymentTarget,
options: &DatabaseOptions,
get_version: &dyn Fn(EventDetails) -> Result<ServiceVersionCheckResult, EngineError>,
) -> Result<TeraContext, EngineError>
where
Database<SCW, M, T>: Service,
{
let event_details = self.get_event_details(Stage::Environment(EnvironmentStep::LoadConfiguration));
let kubernetes = target.kubernetes;
let environment = target.environment;
let mut context = default_tera_context(self, kubernetes, environment);
// we need the kubernetes config file to store tfstates file in kube secrets
let kube_config_file_path = kubernetes.get_kubeconfig_file_path()?;
context.insert("kubeconfig_path", &kube_config_file_path);
kubectl::kubectl_exec_create_namespace_without_labels(
environment.namespace(),
kube_config_file_path.as_str(),
kubernetes.cloud_provider().credentials_environment_variables(),
);
context.insert("namespace", environment.namespace());
let version = get_version(event_details)?.matched_version();
context.insert("version_major", &version.to_major_version_string());
context.insert("version", &version.to_string()); // Scaleway needs to have major version only
for (k, v) in kubernetes.cloud_provider().tera_context_environment_variables() {
context.insert(k, v);
}
context.insert("kubernetes_cluster_id", kubernetes.id());
context.insert("kubernetes_cluster_name", kubernetes.name());
context.insert("fqdn_id", self.fqdn_id.as_str());
context.insert("fqdn", self.fqdn(target, &self.fqdn, M::is_managed()).as_str());
context.insert("service_name", self.fqdn_id.as_str());
context.insert("database_name", self.sanitized_name().as_str());
context.insert("database_db_name", self.name());
context.insert("database_login", options.login.as_str());
context.insert("database_password", options.password.as_str());
context.insert("database_port", &self.private_port());
context.insert("database_disk_size_in_gib", &options.disk_size_in_gib);
context.insert("database_instance_type", &self.database_instance_type);
context.insert("database_disk_type", &options.database_disk_type);
context.insert("database_ram_size_in_mib", &self.total_ram_in_mib);
context.insert("database_total_cpus", &self.total_cpus);
context.insert("database_fqdn", &options.host.as_str());
context.insert("database_id", &self.id());
context.insert("tfstate_suffix_name", &get_tfstate_suffix(self));
context.insert("tfstate_name", &get_tfstate_name(self));
context.insert("publicly_accessible", &options.publicly_accessible);
context.insert("activate_high_availability", &options.activate_high_availability);
context.insert("activate_backups", &options.activate_backups);
context.insert("delete_automated_backups", &self.context().is_test_cluster());
if self.context.resource_expiration_in_seconds().is_some() {
context.insert("resource_expiration_in_seconds", &self.context.resource_expiration_in_seconds())
}
Ok(context)
}
}
////////////////////////////////////////////////////////////////////////:
// POSTGRES SQL
impl ToTeraContext for Database<SCW, Managed, PostgresSQL>
where
PostgresSQL: DatabaseType<SCW, Managed>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let check_version = |event_details| {
check_service_version(
pick_managed_postgres_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_scaleway_managed(target, &self.options, &check_version)
}
}
impl ToTeraContext for Database<SCW, Container, PostgresSQL>
where
PostgresSQL: DatabaseType<SCW, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let _check_version = |event_details| {
check_service_version(
get_self_hosted_postgres_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_container(target, &self.options)
}
}
////////////////////////////////////////////////////////////////////////:
// MySQL
impl ToTeraContext for Database<SCW, Managed, MySQL>
where
MySQL: DatabaseType<SCW, Managed>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let check_version = |event_details| {
check_service_version(
pick_managed_mysql_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_scaleway_managed(target, &self.options, &check_version)
}
}
impl ToTeraContext for Database<SCW, Container, MySQL>
where
MySQL: DatabaseType<SCW, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let _check_version = |event_details| {
check_service_version(
get_self_hosted_mysql_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_container(target, &self.options)
}
}
////////////////////////////////////////////////////////////////////////:
// MongoDB
impl ToTeraContext for Database<SCW, Container, MongoDB>
where
MongoDB: DatabaseType<SCW, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let _check_version = |event_details| {
check_service_version(
get_self_hosted_mongodb_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_container(target, &self.options)
}
}
////////////////////////////////////////////////////////////////////////:
// Redis
impl ToTeraContext for Database<SCW, Container, Redis>
where
Redis: DatabaseType<SCW, Container>,
{
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError> {
let _check_version = |event_details| {
check_service_version(
get_self_hosted_redis_version(self.version.to_string()),
self,
event_details,
self.logger(),
)
};
self.to_tera_context_for_container(target, &self.options)
}
}

View File

@@ -0,0 +1,36 @@
use crate::errors::CommandError;
use crate::models::database_utils::get_supported_version_to_use;
use std::collections::HashMap;
pub(super) fn pick_managed_postgres_version(requested_version: String) -> Result<String, CommandError> {
// Scaleway supported postgres versions
// https://api.scaleway.com/rdb/v1/regions/fr-par/database-engines
let mut supported_postgres_versions = HashMap::new();
// {"name":"PostgreSQL","version":"13","end_of_life":"2025-11-13T00:00:00Z"}
// {"name":"PostgreSQL","version":"12","end_of_life":"2024-11-14T00:00:00Z"}
// {"name":"PostgreSQL","version":"11","end_of_life":"2023-11-09T00:00:00Z"}
// {"name":"PostgreSQL","version":"10","end_of_life":"2022-11-10T00:00:00Z"}
supported_postgres_versions.insert("10".to_string(), "10".to_string());
supported_postgres_versions.insert("10.0".to_string(), "10.0".to_string());
supported_postgres_versions.insert("11".to_string(), "11".to_string());
supported_postgres_versions.insert("11.0".to_string(), "11.0".to_string());
supported_postgres_versions.insert("12".to_string(), "12".to_string());
supported_postgres_versions.insert("12.0".to_string(), "12.0".to_string());
supported_postgres_versions.insert("13".to_string(), "13".to_string());
supported_postgres_versions.insert("13.0".to_string(), "13.0".to_string());
get_supported_version_to_use("RDB postgres", supported_postgres_versions, requested_version)
}
pub(super) fn pick_managed_mysql_version(requested_version: String) -> Result<String, CommandError> {
// Scaleway supported MySQL versions
// https://api.scaleway.com/rdb/v1/regions/fr-par/database-engines
let mut supported_mysql_versions = HashMap::new();
// {"name": "MySQL", "version":"8","end_of_life":"2026-04-01T00:00:00Z"}
supported_mysql_versions.insert("8".to_string(), "8".to_string());
supported_mysql_versions.insert("8.0".to_string(), "8.0".to_string());
get_supported_version_to_use("RDB MySQL", supported_mysql_versions, requested_version)
}

View File

@@ -1,4 +1,6 @@
mod application;
mod database;
mod database_utils;
mod router;
use crate::errors::CommandError;
@@ -33,7 +35,7 @@ impl CloudProvider for SCW {
"Scaleway Container Registry"
}
fn helm_directory_name() -> &'static str {
fn lib_directory_name() -> &'static str {
"scaleway"
}
}

View File

@@ -1,5 +1,10 @@
use serde_derive::{Deserialize, Serialize};
use std::fmt;
use std::fmt::Write;
use std::str::FromStr;
use crate::cloud_provider::DeploymentTarget;
use crate::errors::EngineError;
use crate::errors::{CommandError, EngineError};
use tera::Context as TeraContext;
// Those types are just marker types that are use to tag our struct/object model
@@ -19,9 +24,106 @@ pub trait CloudProvider {
fn full_name() -> &'static str;
fn registry_short_name() -> &'static str;
fn registry_full_name() -> &'static str;
fn helm_directory_name() -> &'static str;
fn lib_directory_name() -> &'static str;
}
pub(crate) trait ToTeraContext {
fn to_tera_context(&self, target: &DeploymentTarget) -> Result<TeraContext, EngineError>;
}
// unfortunately some proposed versions are not SemVer like Elasticache (6.x)
// this is why we need ot have our own structure
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct VersionsNumber {
pub(crate) major: String,
pub(crate) minor: Option<String>,
pub(crate) patch: Option<String>,
pub(crate) suffix: Option<String>,
}
impl VersionsNumber {
pub fn new(major: String, minor: Option<String>, patch: Option<String>, suffix: Option<String>) -> Self {
VersionsNumber {
major,
minor,
patch,
suffix,
}
}
pub fn to_major_version_string(&self) -> String {
self.major.clone()
}
pub fn to_major_minor_version_string(&self, default_minor: &str) -> String {
let test = format!(
"{}.{}",
self.major.clone(),
self.minor.as_ref().unwrap_or(&default_minor.to_string())
);
test
}
}
impl FromStr for VersionsNumber {
type Err = CommandError;
fn from_str(version: &str) -> Result<Self, Self::Err> {
if version.trim() == "" {
return Err(CommandError::new_from_safe_message("version cannot be empty".to_string()));
}
let mut version_split = version.splitn(4, '.').map(|v| v.trim());
let major = match version_split.next() {
Some(major) => {
let major = major.to_string();
major.replace('v', "")
}
None => {
return Err(CommandError::new_from_safe_message(format!(
"please check the version you've sent ({}), it can't be checked",
version
)))
}
};
let minor = version_split.next().map(|minor| {
let minor = minor.to_string();
minor.replace('+', "")
});
let patch = version_split.next().map(|patch| patch.to_string());
let suffix = version_split.next().map(|suffix| suffix.to_string());
// TODO(benjaminch): Handle properly the case where versions are empty
// eq. 1..2
Ok(VersionsNumber::new(major, minor, patch, suffix))
}
}
impl fmt::Display for VersionsNumber {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.major)?;
if let Some(minor) = &self.minor {
f.write_char('.')?;
f.write_str(minor)?;
}
if let Some(patch) = &self.patch {
f.write_char('.')?;
f.write_str(patch)?;
}
if let Some(suffix) = &self.suffix {
f.write_char('.')?;
f.write_str(suffix)?;
}
Ok(())
}
}

View File

@@ -323,7 +323,7 @@ fn postgresql_deploy_a_working_environment_and_redeploy() {
assert!(matches!(ret, TransactionResult::Ok));
// TO CHECK: DATABASE SHOULDN'T BE RESTARTED AFTER A REDEPLOY
let database_name = format!("postgresql{}-0", &environment_check.databases[0].name);
let database_name = format!("postgresql{}-0", &environment_check.databases[0].id);
match is_pod_restarted_env(context, Kind::Aws, environment_check, database_name.as_str(), secrets) {
(true, _) => assert!(true),
(false, _) => assert!(false),

View File

@@ -360,7 +360,7 @@ fn postgresql_deploy_a_working_environment_and_redeploy() {
assert!(matches!(result, TransactionResult::Ok));
// TO CHECK: DATABASE SHOULDN'T BE RESTARTED AFTER A REDEPLOY
let database_name = format!("postgresql-{}-0", &environment_check.databases[0].name);
let database_name = format!("postgresql-{}-0", &environment_check.databases[0].id);
match is_pod_restarted_env(
context.clone(),
ProviderKind::Scw,