Files
engine/src/cloud_provider/aws/application.rs

500 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use serde::{Deserialize, Serialize};
use tera::Context as TeraContext;
use crate::build_platform::Image;
use crate::cloud_provider::aws::{common, AWS};
use crate::cloud_provider::environment::Environment;
use crate::cloud_provider::kubernetes::Kubernetes;
use crate::cloud_provider::service::{
Action, Application as CApplication, Create, Delete, Pause, Service, ServiceType,
StatelessService,
};
use crate::cloud_provider::DeploymentTarget;
use crate::cmd::helm::Timeout;
use crate::constants::{AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY};
use crate::error::{
cast_simple_error_to_engine_error, EngineError, EngineErrorCause,
};
use crate::models::Context;
use crate::cmd::structs::LabelsContent;
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Application {
context: Context,
id: String,
action: Action,
name: String,
private_port: Option<u16>,
total_cpus: String,
cpu_burst: String,
total_ram_in_mib: u32,
total_instances: u16,
start_timeout_in_seconds: u32,
image: Image,
storage: Vec<Storage>,
environment_variables: Vec<EnvironmentVariable>,
}
impl Application {
pub fn new(
context: Context,
id: &str,
action: Action,
name: &str,
private_port: Option<u16>,
total_cpus: String,
cpu_burst: String,
total_ram_in_mib: u32,
total_instances: u16,
start_timeout_in_seconds: u32,
image: Image,
storage: Vec<Storage>,
environment_variables: Vec<EnvironmentVariable>,
) -> Self {
Application {
context,
id: id.to_string(),
action,
name: name.to_string(),
private_port,
total_cpus,
cpu_burst,
total_ram_in_mib,
total_instances,
start_timeout_in_seconds,
image,
storage,
environment_variables,
}
}
fn helm_release_name(&self) -> String {
crate::string::cut(format!("application-{}-{}", self.name(), self.id()), 50)
}
fn workspace_directory(&self) -> String {
crate::fs::workspace_directory(
self.context.workspace_root_dir(),
self.context.execution_id(),
format!("applications/{}", self.name()),
)
}
fn context(&self, kubernetes: &dyn Kubernetes, environment: &Environment) -> TeraContext {
let mut context = self.default_tera_context(kubernetes, environment);
let commit_id = self.image().commit_id.as_str();
context.insert("helm_app_version", &commit_id[..7]);
match &self.image().registry_url {
Some(registry_url) => context.insert("image_name_with_tag", registry_url.as_str()),
None => {
let image_name_with_tag = self.image().name_with_tag();
warn!("there is no registry url, use image name with tag with the default container registry: {}", image_name_with_tag.as_str());
context.insert("image_name_with_tag", image_name_with_tag.as_str());
}
}
let environment_variables = self
.environment_variables
.iter()
.map(|ev| EnvironmentVariableDataTemplate {
key: ev.key.clone(),
value: ev.value.clone(),
})
.collect::<Vec<_>>();
context.insert("environment_variables", &environment_variables);
let storage = self
.storage
.iter()
.map(|s| StorageDataTemplate {
id: s.id.clone(),
name: s.name.clone(),
storage_type: match s.storage_type {
StorageType::SC1 => "sc1",
StorageType::ST1 => "st1",
StorageType::GP2 => "gp2",
StorageType::IO1 => "io1",
}
.to_string(),
size_in_gib: s.size_in_gib,
mount_point: s.mount_point.clone(),
snapshot_retention_in_days: s.snapshot_retention_in_days,
})
.collect::<Vec<_>>();
let is_storage = storage.len() > 0;
context.insert("storage", &storage);
context.insert("is_storage", &is_storage);
context.insert("clone", &false);
context.insert("start_timeout_in_seconds", &self.start_timeout_in_seconds);
if self.context.resource_expiration_in_seconds().is_some() {
context.insert(
"resource_expiration_in_seconds",
&self.context.resource_expiration_in_seconds(),
)
}
context
}
fn delete(&self, target: &DeploymentTarget, is_error: bool) -> Result<(), EngineError> {
let (kubernetes, environment) = match target {
DeploymentTarget::ManagedServices(k, env) => (*k, *env),
DeploymentTarget::SelfHosted(k, env) => (*k, *env),
};
let workspace_dir = self.workspace_directory();
let helm_release_name = self.helm_release_name();
let selector = format!("app={}", self.name());
if is_error {
let _ = cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
common::get_stateless_resource_information(
kubernetes,
environment,
workspace_dir.as_str(),
selector.as_str(),
),
)?;
}
// clean the resource
let _ = cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
common::do_stateless_service_cleanup(
kubernetes,
environment,
workspace_dir.as_str(),
helm_release_name.as_str(),
),
)?;
Ok(())
}
}
impl crate::cloud_provider::service::Application for Application {
fn image(&self) -> &Image {
&self.image
}
fn set_image(&mut self, image: Image) {
self.image = image;
}
}
impl StatelessService for Application {}
impl Service for Application {
fn context(&self) -> &Context {
&self.context
}
fn service_type(&self) -> ServiceType {
ServiceType::Application
}
fn id(&self) -> &str {
self.id.as_str()
}
fn name(&self) -> &str {
self.name.as_str()
}
fn version(&self) -> &str {
self.image.commit_id.as_str()
}
fn action(&self) -> &Action {
&self.action
}
fn private_port(&self) -> Option<u16> {
self.private_port
}
fn total_cpus(&self) -> String {
self.total_cpus.to_string()
}
fn total_ram_in_mib(&self) -> u32 {
self.total_ram_in_mib
}
fn total_instances(&self) -> u16 {
self.total_instances
}
}
impl Create for Application {
fn on_create(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
info!("AWS.application.on_create() called for {}", self.name());
let (kubernetes, environment) = match target {
DeploymentTarget::ManagedServices(k, env) => (*k, *env),
DeploymentTarget::SelfHosted(k, env) => (*k, *env),
};
let aws = kubernetes
.cloud_provider()
.as_any()
.downcast_ref::<AWS>()
.unwrap();
let context = self.context(kubernetes, environment);
let workspace_dir = self.workspace_directory();
let from_dir = format!("{}/aws/charts/q-application", self.context.lib_root_dir());
let _ = cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
crate::template::generate_and_copy_all_files_into_dir(
from_dir.as_str(),
workspace_dir.as_str(),
&context,
),
)?;
// render
// TODO check the rendered files?
let helm_release_name = self.helm_release_name();
let aws_credentials_envs = vec![
(AWS_ACCESS_KEY_ID, aws.access_key_id.as_str()),
(AWS_SECRET_ACCESS_KEY, aws.secret_access_key.as_str()),
];
let kubernetes_config_file_path = cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
common::kubernetes_config_path(
workspace_dir.as_str(),
environment.organization_id.as_str(),
kubernetes.id(),
aws.access_key_id.as_str(),
aws.secret_access_key.as_str(),
kubernetes.region(),
),
)?;
// define labels to add to namespace
let namespace_labels = match self.context.resource_expiration_in_seconds() {
Some(v) => Some(vec![(LabelsContent{
name: "ttl".to_string(),
value: format!{"{}", self.context.resource_expiration_in_seconds().unwrap()},
})]),
None => None,
};
// create a namespace with labels if do not exists
let _ = cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
crate::cmd::kubectl::kubectl_exec_create_namespace(
kubernetes_config_file_path.as_str(),
environment.namespace(),
namespace_labels,
aws_credentials_envs.clone(),
),
)?;
// do exec helm upgrade and return the last deployment status
let helm_history_row = cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
crate::cmd::helm::helm_exec_with_upgrade_history(
kubernetes_config_file_path.as_str(),
environment.namespace(),
helm_release_name.as_str(),
workspace_dir.as_str(),
Timeout::Value(self.start_timeout_in_seconds),
aws_credentials_envs.clone(),
),
)?;
// check deployment status
if helm_history_row.is_none() || !helm_history_row.unwrap().is_successfully_deployed() {
return Err(self.engine_error(
EngineErrorCause::User(
"Your application didn't start for some reason. \
Are you sure your application is correctly running? You can give a try by running \
locally `qovery run`. You can also check the application log from the web \
interface or the CLI with `qovery log`",
),
format!("Application {} has failed to start ", self.name_with_id()),
));
}
// check app status
let selector = format!("app={}", self.name());
let _ = cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
crate::cmd::kubectl::kubectl_exec_is_pod_ready_with_retry(
kubernetes_config_file_path.as_str(),
environment.namespace(),
selector.as_str(),
aws_credentials_envs,
),
)?;
Ok(())
}
fn on_create_check(&self) -> Result<(), EngineError> {
Ok(())
}
fn on_create_error(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
warn!(
"AWS.application.on_create_error() called for {}",
self.name()
);
let (kubernetes, environment) = match target {
DeploymentTarget::ManagedServices(k, env) => (*k, *env),
DeploymentTarget::SelfHosted(k, env) => (*k, *env),
};
let workspace_dir = self.workspace_directory();
let aws = kubernetes
.cloud_provider()
.as_any()
.downcast_ref::<AWS>()
.unwrap();
let aws_credentials_envs = vec![
(AWS_ACCESS_KEY_ID, aws.access_key_id.as_str()),
(AWS_SECRET_ACCESS_KEY, aws.secret_access_key.as_str()),
];
let kubernetes_config_file_path = cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
common::kubernetes_config_path(
workspace_dir.as_str(),
environment.organization_id.as_str(),
kubernetes.id(),
aws.access_key_id.as_str(),
aws.secret_access_key.as_str(),
kubernetes.region(),
),
)?;
let helm_release_name = self.helm_release_name();
let history_rows = cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
crate::cmd::helm::helm_exec_history(
kubernetes_config_file_path.as_str(),
environment.namespace(),
helm_release_name.as_str(),
aws_credentials_envs.clone(),
),
)?;
if history_rows.len() == 1 {
cast_simple_error_to_engine_error(
self.engine_error_scope(),
self.context.execution_id(),
crate::cmd::helm::helm_exec_uninstall(
kubernetes_config_file_path.as_str(),
environment.namespace(),
helm_release_name.as_str(),
aws_credentials_envs.clone(),
),
)?;
}
Ok(())
}
}
impl Pause for Application {
fn on_pause(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
info!("AWS.application.on_pause() called for {}", self.name());
self.delete(target, false)
}
fn on_pause_check(&self) -> Result<(), EngineError> {
Ok(())
}
fn on_pause_error(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
warn!(
"AWS.application.on_pause_error() called for {}",
self.name()
);
self.delete(target, true)
}
}
impl Delete for Application {
fn on_delete(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
info!("AWS.application.on_delete() called for {}", self.name());
self.delete(target, false)
}
fn on_delete_check(&self) -> Result<(), EngineError> {
Ok(())
}
fn on_delete_error(&self, target: &DeploymentTarget) -> Result<(), EngineError> {
warn!(
"AWS.application.on_delete_error() called for {}",
self.name()
);
self.delete(target, true)
}
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct EnvironmentVariable {
pub key: String,
pub value: String,
}
#[derive(Serialize, Deserialize)]
struct EnvironmentVariableDataTemplate {
pub key: String,
pub value: String,
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Storage {
pub id: String,
pub name: String,
pub storage_type: StorageType,
pub size_in_gib: u16,
pub mount_point: String,
pub snapshot_retention_in_days: u16,
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub enum StorageType {
SC1,
ST1,
GP2,
IO1,
}
#[derive(Serialize, Deserialize)]
struct StorageDataTemplate {
pub id: String,
pub name: String,
pub storage_type: String,
pub size_in_gib: u16,
pub mount_point: String,
pub snapshot_retention_in_days: u16,
}