Permalink
Browse files

Generate and using handlebars templating for application companions

  • Loading branch information...
schrieveslaach committed Dec 11, 2018
1 parent a560ef4 commit 7c0ad97c234cd0a587ca2bd0a121954001e0220b

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -9,6 +9,7 @@ crossbeam = "0.4"
crossbeam-utils = "0.5"
failure = "0.1"
goji = "0.2"
handlebars = "1.1"
hyper = "0.10"
log = "0.4"
multimap = "0.4"
@@ -67,6 +67,8 @@ remote.services={{#each services~}}

The list of available handlebars variables:

- `application`: The companion's application information
- `name`: The application name
- `applicationPath`: The root path to the application.
- `services`: An array of the services of the application. Each element has following structure:
- `name`: The service name which is equivalent to the network alias
@@ -75,13 +77,13 @@ The list of available handlebars variables:

### Service Based

The service-based companions works the in the same way as the application-based services
The service-based companions works the in the same way as the application-based services. Make sure, that the `serviceName` is unique by using the handlebars templating.

```toml
[[companions]]
[[companions.services]]
serviceName = 'postgres'
image = 'private.example.com/library/opendid:latest'
serviceName = '{{service.name}}-db'
image = 'postgres:11'
env = [ 'KEY=VALUE' ]
[companions.services.postgres.volumes]
"/path/to/volume.properties" == "…"
@@ -92,7 +94,8 @@ env = [ 'KEY=VALUE' ]

The list of available handlebars variables:

- `applicationPath`: The root path to the application.
- `application`: The companion's application information
- `name`: The application name
- `service`: The companion's service containing following fields:
- `name`: The service name which is equivalent to the network alias
- `port`: The exposed port of the service
@@ -29,6 +29,7 @@ extern crate crossbeam_utils;
#[macro_use]
extern crate failure;
extern crate goji;
extern crate handlebars;
extern crate hyper;
#[macro_use]
extern crate log;
@@ -126,6 +126,10 @@ impl ServiceConfig {
)
}

pub fn set_service_name(&mut self, service_name: &String) {
self.service_name = service_name.clone()
}

pub fn get_service_name(&self) -> &String {
&self.service_name
}
@@ -137,6 +141,10 @@ impl ServiceConfig {
}
}

pub fn set_env(&mut self, env: &Option<Vec<String>>) {
self.env = env.clone();
}

pub fn get_env(&self) -> Option<Vec<String>> {
match &self.env {
None => None,
@@ -229,7 +237,7 @@ impl Serialize for Service {
}
}

#[derive(Serialize, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum ContainerType {
Instance,
Replica,
@@ -25,12 +25,14 @@
*/
use std::convert::From;

use handlebars::TemplateRenderError;
use models::service::{ContainerType, Service, ServiceConfig, ServiceError};
use multimap::MultiMap;
use services::config_service::{Config, ConfigError};

use services::docker::docker_infrastructure::DockerInfrastructure;
use services::infrastructure::Infrastructure;
use services::service_templating::apply_templating_for_application_companion;

pub struct AppsService {
config: Config,
@@ -45,13 +47,18 @@ impl AppsService {
})
}

/// Analyzes running containers and returns a map of `review-app-name` with the
/// Analyzes running containers and returns a map of `app-name` with the
/// corresponding list of `Service`s.
pub fn get_apps(&self) -> Result<MultiMap<String, Service>, AppsServiceError> {
Ok(self.infrastructure.get_services()?)
}

/// Creates or updates a review app with the given service configurations
/// Creates or updates a app to review with the given service configurations.
///
/// The list of given services will be extended with:
/// - the replications from the running template application (e.g. master)
/// - the application companions (see README)
/// - the service companions (see README)
pub fn create_or_update(
&self,
app_name: &String,
@@ -80,9 +87,14 @@ impl AppsService {
}
}

// TODO configs.extend(self.get_application_companion_configurations()?);
// config
// .get_application_companion_configs()?
for app_companion_config in self.config.get_application_companion_configs()? {
let applied_template_config = apply_templating_for_application_companion(
&app_companion_config,
app_name,
&configs,
);
configs.push(applied_template_config?);
}

let services = self.infrastructure.start_services(
app_name,
@@ -93,7 +105,7 @@ impl AppsService {
Ok(services)
}

/// Deletes all services for the given `app_name` (review app name)
/// Deletes all services for the given `app_name`.
pub fn delete_app(&self, app_name: &String) -> Result<Vec<Service>, AppsServiceError> {
match self.infrastructure.get_services()?.get_vec(app_name) {
None => Err(AppsServiceError::AppNotFound(app_name.clone())),
@@ -113,6 +125,7 @@ pub enum AppsServiceError {
InfrastructureError(failure::Error),
/// Will be used if the service configuration cannot be loaded.
InvalidServerConfiguration(ConfigError),
InvalidTemplateFormat(TemplateRenderError)
}

impl From<ConfigError> for AppsServiceError {
@@ -126,3 +139,10 @@ impl From<failure::Error> for AppsServiceError {
AppsServiceError::InfrastructureError(error)
}
}

impl From<TemplateRenderError> for AppsServiceError {
fn from(error: TemplateRenderError) -> Self {
AppsServiceError::InvalidTemplateFormat(error)
}
}

@@ -34,7 +34,7 @@ use serde::{de, Deserialize, Deserializer};
use toml::de::Error as TomlError;
use toml::from_str;

use models::service::ServiceConfig;
use models::service::{ContainerType, ServiceConfig};

#[derive(Clone, Deserialize)]
pub struct ContainerConfig {
@@ -132,7 +132,9 @@ impl Config {
.filter(|(k, _)| k.as_str() == "application")
.map(|(_, v)| v)
{
companions.push(ServiceConfig::try_from(companion)?);
let mut config = ServiceConfig::try_from(companion)?;
config.set_container_type(ContainerType::ApplicationCompanion);
companions.push(config);
}

Ok(companions)
@@ -253,6 +255,10 @@ mod tests {
"private.example.com/library/opendid:latest"
);
assert_eq!(config.get_image_tag(), "latest");
assert_eq!(
config.get_container_type(),
&ContainerType::ApplicationCompanion
);
});
}

@@ -32,6 +32,14 @@ use models::service::{Service, ServiceConfig};
pub trait Infrastructure {
fn get_services(&self) -> Result<MultiMap<String, Service>, Error>;

/// Starts the services of the given set of `ServiceConfig`.
///
/// The implementation must ensure that:
/// - the services are able to communicate with each other with the service name. For example,
/// they must be able the execute `ping <service_name>`.
/// - the services must be deployed once. If a service is already running, it must be redeployed.
/// - the services must be discoverable for further calls. For example, `self.stop_services(...)`
/// must be able to find the corresponding services.
fn start_services(
&self,
app_name: &String,
@@ -27,3 +27,4 @@ pub mod apps_service;
pub mod config_service;
pub mod docker;
pub mod infrastructure;
pub mod service_templating;
@@ -0,0 +1,161 @@
/*-
* ========================LICENSE_START=================================
* PREvant REST API
* %%
* Copyright (C) 2018 aixigo AG
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* =========================LICENSE_END==================================
*/
use handlebars::{Handlebars, TemplateRenderError};
use models::service::{ContainerType, ServiceConfig};

pub fn apply_templating_for_application_companion(
service_config: &ServiceConfig,
app_name: &String,
service_configs: &Vec<ServiceConfig>,
) -> Result<ServiceConfig, TemplateRenderError> {
let parameters = TemplateParameters {
application: ApplicationTemplateParameter {
name: app_name.clone(),
},
services: Some(service_configs.iter().map(|c| ServiceTemplateParameter {
name: c.get_service_name().clone(),
container_type: c.get_container_type().clone(),
port: 80
}).collect()),
service: None,
};

let reg = Handlebars::new();

let mut templated_config = service_config.clone();
templated_config
.set_service_name(&reg.render_template(service_config.get_service_name(), &parameters)?);

if let Some(env) = service_config.get_env() {
let mut templated_env = Vec::new();

for e in env {
templated_env.push(reg.render_template(&e, &parameters)?);
}

templated_config.set_env(&Some(templated_env));
}

Ok(templated_config)
}

#[derive(Serialize)]
struct TemplateParameters {
application: ApplicationTemplateParameter,
services: Option<Vec<ServiceTemplateParameter>>,
service: Option<ServiceTemplateParameter>,
}

#[derive(Serialize)]
struct ApplicationTemplateParameter {
name: String,
}

#[derive(Serialize)]
struct ServiceTemplateParameter {
name: String,
port: u16,
#[serde(rename = "type")]
container_type: ContainerType,
}

#[cfg(test)]
mod tests {

use super::*;

#[test]
fn should_apply_app_companion_templating_with_service_name() {
let env = vec![];
let config = ServiceConfig::new(
&String::from("postgres-{{application.name}}"),
&String::from("postgres"),
Some(env),
);

let service_configs = vec![];
let templated_config = apply_templating_for_application_companion(
&config,
&String::from("master"),
&service_configs,
)
.unwrap();

assert_eq!(templated_config.get_service_name(), "postgres-master");
}

#[test]
fn should_apply_app_companion_templating_with_envs() {
let env = vec![String::from(
r#"DATABASE_SCHEMAS=
{{~#each services~}}
{{~name~}},
{{~/each~}}"#,
)];

let config = ServiceConfig::new(
&String::from("postgres-db"),
&String::from("postgres"),
Some(env),
);

let service_configs = vec![
ServiceConfig::new(&String::from("service-a"), &String::from("service"), None),
ServiceConfig::new(&String::from("service-b"), &String::from("service"), None)
];
let templated_config = apply_templating_for_application_companion(
&config,
&String::from("master"),
&service_configs,
).unwrap();

assert_eq!(templated_config.get_env().unwrap().get(0).unwrap(), "DATABASE_SCHEMAS=service-a,service-b,");
}

#[test]
fn should_not_apply_app_companion_templating_with_invalid_envs() {
let env = vec![String::from(
r#"DATABASE_SCHEMAS=
{{~each services~}}
{{~name~}},
{{~/each~}}"#,
)];

let config = ServiceConfig::new(
&String::from("postgres-db"),
&String::from("postgres"),
Some(env),
);

let templated_config = apply_templating_for_application_companion(
&config,
&String::from("master"),
&vec![],
);

assert_eq!(templated_config.is_err(), true);
}
}

0 comments on commit 7c0ad97

Please sign in to comment.