Skip to content

Commit

Permalink
Bootstrap Companions in Kubernetes Environments
Browse files Browse the repository at this point in the history
This commit introduces the configuration `bootstrapping.containers`
which provides a way to parse the configuration of application wide
companions because the current available configuration of companions if
quite limiting (Current backends, Docker and Kubernetes, offer way more
options than the PREvant configuration object allows). For example,
PREvant was limited to self-contained applications where each
microservice only relies on interactions via network API calls (REST,
database connections, messaging, etc.) With this commit PREvant is now
able to deploy application companions that are more powerful than the
PREvant configuration in Kubernetes backends.

If `bootstrapping.containers` is defined, PREvant will start one or more
containers on the infrastructure backend that are expected to generate
Kubernetes manifests as output on standard out (stdout) that will be
parsed by PREvant and supported are:

 - roles and role bindings
 - config maps and secrets
 - service accounts
 - persistent volume claims
 - services
 - pods, deployments, stateful sets, and jobs

Then before deploying these manifests PREvant merges all objects with
the objects generated from the HTTP request payload. Thus you can add or
overwrite configurations. For example, you can change the image used or
an environment variable. If you overwrite any configuration the
companion will be turned into an instance (as PREvant did before).

Ingresses won't be deployed if the bootstrap container outputs one of
these. Instead they will be parsed and if they use the ingress class
`nginx` they will be transformed into Traefik ingresses and middlewares
so that the microservices will be available via web interface.

This approach make #143 obsolete and fixes #123 and contributes to #146.
  • Loading branch information
schrieveslaach committed Jan 11, 2024
1 parent 40c46a5 commit 9fb15a7
Show file tree
Hide file tree
Showing 20 changed files with 3,460 additions and 697 deletions.
355 changes: 152 additions & 203 deletions api/Cargo.lock

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ futures = { version = "0.3", features = ["compat"] }
handlebars = "4.5"
http-api-problem = "0.57"
jira_query = "1.3"
k8s-openapi = { version = "0.18", default-features = false, features = ["v1_24"] }
kube = { version = "0.84", default-features = false, features = ["client", "derive", "rustls-tls"] }
k8s-openapi = { version = "0.20", default-features = false, features = ["v1_24"] }
kube = { version = "0.87", default-features = false, features = ["client", "derive", "rustls-tls"] }
lazy_static = "1.4"
log = "0.4"
multimap = "0.9"
pest = "2.6"
pest_derive = "2.6"
regex = "1.9"
regex = "1.10"
regex-syntax = "0.8"
reqwest = { version = "0.11", features = ["json"] }
rocket = { version = "0.5", features = ["json"] }
schemars = "0.8"
Expand All @@ -45,9 +46,9 @@ serde_json = "1.0"
serde_regex = "1.1"
serde_yaml = "0.9"
tokio = { version = "1.29", features = ["macros", "rt", "rt-multi-thread", "sync", "time"] }
toml = "0.7"
toml = "0.8"
url = { version = "2.4", features = ["serde"] }
uuid = { version = "1.3", features = ["serde", "v4"] }
uuid = { version = "1.5", features = ["serde", "v4"] }
yansi = "0.5"


Expand Down
28 changes: 17 additions & 11 deletions api/src/apps/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ impl AppsService {
let deployment_unit = if let Ok(Some(base_traefik_ingress_route)) =
self.infrastructure.base_traefik_ingress_route().await
{
trace!(
"The base URL for {app_name} is: {:?}",
base_traefik_ingress_route
.to_url()
.map(|url| url.to_string())
);
deployment_unit_builder
.apply_base_traefik_ingress_route(base_traefik_ingress_route)
.build()
Expand Down Expand Up @@ -511,7 +517,7 @@ mod tests {
let apps = AppsService::new(config, infrastructure)?;

apps.create_or_update(
&AppName::from_str("master").unwrap(),
&AppName::master(),
&AppStatusChangeId::new(),
None,
&vec![sc!("service-a"), sc!("service-b")],
Expand All @@ -521,7 +527,7 @@ mod tests {
apps.create_or_update(
&AppName::from_str("branch").unwrap(),
&AppStatusChangeId::new(),
Some(AppName::from_str("master").unwrap()),
Some(AppName::master()),
&vec![sc!("service-b")],
)
.await?;
Expand Down Expand Up @@ -563,7 +569,7 @@ mod tests {
apps.create_or_update(
&AppName::from_str("branch").unwrap(),
&AppStatusChangeId::new(),
Some(AppName::from_str("master").unwrap()),
Some(AppName::master()),
&vec![sc!("service-a")],
)
.await?;
Expand Down Expand Up @@ -596,7 +602,7 @@ mod tests {
let apps = AppsService::new(config, infrastructure)?;

apps.create_or_update(
&AppName::from_str("master").unwrap(),
&AppName::master(),
&AppStatusChangeId::new(),
None,
&vec![sc!("mariadb")],
Expand Down Expand Up @@ -660,7 +666,7 @@ mod tests {
let infrastructure = Box::new(Dummy::new());
let apps = AppsService::new(config, infrastructure)?;

let app_name = AppName::from_str("master").unwrap();
let app_name = AppName::master();

apps.create_or_update(
&app_name,
Expand Down Expand Up @@ -857,7 +863,7 @@ Log msg 3 of service-a of app master

let infrastructure = Box::new(Dummy::new());
let apps = AppsService::new(config, infrastructure)?;
let app_name = AppName::from_str("master").unwrap();
let app_name = AppName::master();

apps.create_or_update(
&app_name,
Expand Down Expand Up @@ -942,7 +948,7 @@ Log msg 3 of service-a of app master
let infrastructure = Box::new(Dummy::with_delay(std::time::Duration::from_millis(500)));
let apps = Arc::new(AppsService::new(config, infrastructure)?);

let app_name = AppName::from_str("master").unwrap();
let app_name = AppName::master();
apps.create_or_update(
&app_name,
&AppStatusChangeId::new(),
Expand All @@ -959,7 +965,7 @@ Log msg 3 of service-a of app master
.unwrap();
rt.block_on(apps_clone.delete_app(&app_name, &AppStatusChangeId::new()))
});
let app_name = AppName::from_str("master").unwrap();
let app_name = AppName::master();
let handle2 = std::thread::spawn(move || {
let rt = runtime::Builder::new_current_thread()
.enable_time()
Expand Down Expand Up @@ -1055,7 +1061,7 @@ Log msg 3 of service-a of app master
"#;

let (_temp_js_file, config) = config_with_deployment_hook(script);
let app_name = &AppName::from_str("master").unwrap();
let app_name = &AppName::master();
let infrastructure = Box::new(Dummy::new());
let apps = AppsService::new(config, infrastructure)?;

Expand Down Expand Up @@ -1087,7 +1093,7 @@ Log msg 3 of service-a of app master
)));
let apps = AppsService::new(Config::default(), infrastructure)?;

let app_name = &AppName::from_str("master").unwrap();
let app_name = &AppName::master();
apps.create_or_update(
&app_name,
&AppStatusChangeId::new(),
Expand Down Expand Up @@ -1129,7 +1135,7 @@ Log msg 3 of service-a of app master
let infrastructure = Box::new(Dummy::new());
let apps = AppsService::new(Config::default(), infrastructure)?;

let app_name = &AppName::from_str("master").unwrap();
let app_name = &AppName::master();
apps.create_or_update(
&app_name,
&AppStatusChangeId::new(),
Expand Down
6 changes: 3 additions & 3 deletions api/src/apps/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ async fn change_status(
)]
async fn logs(
app_name: Result<AppName, AppNameError>,
service_name: String,
service_name: &str,
since: Option<String>,
limit: Option<usize>,
apps: &State<Arc<Apps>>,
Expand All @@ -205,13 +205,13 @@ async fn logs(
let limit = limit.unwrap_or(20_000);

let log_chunk = apps
.get_logs(&app_name, &service_name, &since, limit)
.get_logs(&app_name, &service_name.to_string(), &since, limit)
.await?;

Ok(LogsResponse {
log_chunk,
app_name,
service_name,
service_name: service_name.to_string(),
limit,
})
}
Expand Down
5 changes: 3 additions & 2 deletions api/src/config/app_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@
* THE SOFTWARE.
* =========================LICENSE_END==================================
*/
use crate::models::AppName;
use regex::Regex;

#[derive(Clone)]
pub(super) struct AppSelector(Regex);

impl AppSelector {
pub fn matches(&self, app_name: &str) -> bool {
pub fn matches(&self, app_name: &AppName) -> bool {
match self.0.captures(app_name) {
None => false,
Some(captures) => captures.get(0).map_or("", |m| m.as_str()) == app_name,
Some(captures) => captures.get(0).map_or("", |m| m.as_str()) == app_name.as_str(),
}
}
}
Expand Down
158 changes: 156 additions & 2 deletions api/src/config/companion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,21 @@
*/
use crate::config::AppSelector;
use crate::models::service::ContainerType;
use crate::models::{Environment, Image, Router, ServiceConfig};
use crate::models::{AppName, Environment, Image, Router, ServiceConfig};
use handlebars::Handlebars;
use secstr::SecUtf8;
use serde_value::Value;
use std::collections::BTreeMap;
use std::path::PathBuf;
use url::Url;

#[derive(Clone, Default, Deserialize)]
pub(super) struct Companions {
#[serde(default)]
bootstrapping: Bootstrapping,
#[serde(flatten)]
companions: BTreeMap<String, Companion>,
}

#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -78,12 +88,52 @@ pub enum DeploymentStrategy {
RedeployNever,
}

#[derive(Clone, Default, Deserialize)]
struct Bootstrapping {
containers: Vec<BootstrappingContainer>,
}

#[derive(Clone, Deserialize)]
pub struct BootstrappingContainer {
image: Image,
#[serde(default)]
args: Vec<String>,
}

impl Companions {
pub(super) fn companion_configs<P>(
&self,
app_name: &AppName,
predicate: P,
) -> Vec<(ServiceConfig, DeploymentStrategy, StorageStrategy)>
where
P: Fn(&Companion) -> bool,
{
self.companions
.iter()
.filter(|(_, companion)| companion.matches_app_name(app_name))
.filter(|(_, companion)| predicate(companion))
.map(|(_, companion)| {
(
ServiceConfig::from(companion.clone()),
companion.deployment_strategy().clone(),
companion.storage_strategy().clone(),
)
})
.collect()
}

pub(super) fn companion_bootstrapping_containers(&self) -> &Vec<BootstrappingContainer> {
&self.bootstrapping.containers
}
}

impl Companion {
pub fn companion_type(&self) -> &CompanionType {
&self.companion_type
}

pub fn matches_app_name(&self, app_name: &str) -> bool {
pub fn matches_app_name(&self, app_name: &AppName) -> bool {
self.app_selector.matches(app_name)
}

Expand Down Expand Up @@ -149,6 +199,42 @@ impl Default for StorageStrategy {
}
}

impl BootstrappingContainer {
pub fn image(&self) -> &Image {
&self.image
}

pub fn templated_args(&self, app_name: &AppName, base_url: &Option<Url>) -> Vec<String> {
let handlebars = Handlebars::new();

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct AppData<'a> {
name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
base_url: &'a Option<Url>,
}
// TODO: apply same pattern as for companions. {{application.name}}, {{service.…}}…
#[derive(Serialize)]
struct Data<'a> {
application: AppData<'a>,
}

let data = Data {
application: AppData {
name: &app_name,
base_url,
},
};

self.args
.iter()
// TODO: handle result
.map(|arg| handlebars.render_template(&arg, &data).unwrap())
.collect()
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -160,6 +246,12 @@ mod tests {
};
}

macro_rules! companions_from_str {
( $config_str:expr ) => {
toml::de::from_str::<Companions>($config_str).unwrap()
};
}

#[test]
fn should_parse_companion_with_required_fields() {
let companion = companion_from_str!(
Expand All @@ -181,4 +273,66 @@ mod tests {
DeploymentStrategy::RedeployAlways
);
}

#[test]
fn should_parse_companion_bootstrap_containers() {
let companions = companions_from_str!(
r#"
[[bootstrapping.containers]]
image = "busybox"
"#
);

let container = &companions.bootstrapping.containers[0];

assert_eq!(container.image, Image::from_str("busybox").unwrap());
assert_eq!(
container.templated_args(&AppName::master(), &None),
Vec::<String>::new()
);
}

#[test]
fn should_parse_companion_bootstrap_containers_and_template_args() {
let companions = companions_from_str!(
r#"
[[bootstrapping.containers]]
image = "busybox"
args = [ "echo", "Hello {{application.name}}" ]
"#
);

let container = &companions.bootstrapping.containers[0];

assert_eq!(container.image, Image::from_str("busybox").unwrap());
assert_eq!(
container.templated_args(&AppName::master(), &None),
vec![String::from("echo"), String::from("Hello master")]
);
}

#[test]
fn should_parse_companion_bootstrap_containers_and_template_url_args() {
let companions = companions_from_str!(
r#"
[[bootstrapping.containers]]
image = "busybox"
args = [ "echo", "Hello {{application.baseUrl}}" ]
"#
);

let container = &companions.bootstrapping.containers[0];

assert_eq!(container.image, Image::from_str("busybox").unwrap());
assert_eq!(
container.templated_args(
&AppName::master(),
&Some(Url::parse("http://example.com").unwrap())
),
vec![
String::from("echo"),
String::from("Hello http://example.com/")
]
);
}
}
Loading

0 comments on commit 9fb15a7

Please sign in to comment.