Skip to content
Permalink
Browse files

Resolve container versions and OpenAPI spec links through host-meta.j…

…son (RFC-6415)

When requesting the list of apps, PREvant tries to resolve .well-known/host-meta.json
for each service which should provide versioning information and the link to the OpenAPI
specification.
  • Loading branch information...
schrieveslaach committed Apr 3, 2019
1 parent 43de4c6 commit 508952d8759f4401dceaed66fcd32d0f0a4694ea
@@ -22,6 +22,40 @@ Now, PREvant is running at [`http://localhost`](http://localhost).

If you want to customize PREvant's behaviour, you can mount a TOML file into the container at the path `/app/config.toml`. You will find more information about the configuration [here](api/README.md).

# Requirements for Your Services

PREvant is able to show the version of your service (build time, version string, and git commit hash) and also to integrate your API specification into the frontend through [Swagger UI](https://swagger.io/tools/swagger-ui/). In order to show the information, PREvant tries to resolve it by using the web-based protocol proposed by [RFC 6415](https://tools.ietf.org/html/rfc6415).

When you request the list of apps and services running through the frontend, PREvant makes a request for each service to the URL `.well-known/host-meta.json` and expects that the resource provides a [host-meta document](http://docs.oasis-open.org/xri/xrd/v1.0/xrd-1.0.html) serialized as JSON:

```json
{
"properties": {
"https://schema.org/softwareVersion": "0.9",
"https://schema.org/dateModified": "2019-04-09T15:31:01.363+0200",
"https://git-scm.com/docs/git-commit": "43de4c6edf3c7ed93cdf8983f1ea7d73115176cc"
},
"links": [
{
"rel": "https://github.com/OAI/OpenAPI-Specification",
"href": "https://example.com/master/service-name/swagger.json"
}
]
}
```

This sample document contains the relevant information displayed in the frontend (each information is optional):

- The software version of the service (see `https://schema.org/softwareVersion`)
- The build time of the service (see `https://schema.org/dateModified`)
- The git commit id of the service (see `https://git-scm.com/docs/git-commit`)
- The link to the API specification (see `https://github.com/OAI/OpenAPI-Specification`)

In order to generate the correct link the API specification PREvant adds following headers to each of these requests:

- [`Forwarded` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded) with `host` and `proto`.
- `X-Forwarded-Prefix` (used by some reverse proxies, cf. [Traefik](https://docs.traefik.io/basics/) and [Zuul](https://cloud.spring.io/spring-cloud-static/Finchley.SR1/multi/multi__router_and_filter_zuul.html)).

# Further Readings

PREvant's concept has been published at the conference [_Microservices 2019_ in Dortmund](https://www.conf-micro.services/2019/). You can read [the abstract here](https://www.conf-micro.services/2019/papers/Microservices_2019_paper_14.pdf).

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -6,6 +6,7 @@ repository = "https://github.com/aixigo/PREvant/"
edition = "2018"

[dependencies]
clap = "2.33"
crossbeam = "0.7"
crossbeam-utils = "0.6"
dkregistry = "0.2"
@@ -15,7 +16,6 @@ futures = "0.1"
handlebars = "1.1"
hyper = "0.10"
log = "0.4"
mockers_derive = "0.13.1"
multimap = "0.4"
serde = "1.0"
serde_derive = "1.0"
@@ -30,6 +30,10 @@ rocket_contrib = "0.4"
rocket-cache-response = "0.5"
url = "1.7"

[dependencies.chrono]
version = "0.4"
features = [ "serde" ]

[dependencies.http-api-problem]
version = "0.12"
features = ["with_rocket"]
@@ -45,6 +49,6 @@ git = "https://github.com/softprops/goji.git"
rev = "1d72ffee98b88321418181998225c700904be414"

[dependencies.reqwest]
version = "0.9.7"
version = "0.9"
default-features = false
features = ["rustls-tls"]
@@ -189,16 +189,37 @@ components:
- app-companion
- service-companion
example: instance
version:
type: object
$ref: '#/components/schemas/Version'
url:
type: string
format: url
versionUrl:
openApiUrl:
type: string
format: url
example: https://speca.io/speca/petstore-api
description: The URL pointing to the OpenAPI specification of the service
required:
- name
- type
- url
- version
Version:
type: object
properties:
gitCommit:
type: string
description: The git commit SHA hash of the service.
example: '43de4c6edf3c7ed93cdf8983f1ea7d73115176cc'
dateModified:
type: string
format: date-time
description: The date and time when this service has been build.
example: '2018-11-13T20:20:39+00:00'
softwareVersion:
type: string
description: The human-readable version string, see [softwareVersion](https://schema.org/softwareVersion).
example: '1.2.3'
ServiceConfiguration:
type: object
properties:
@@ -43,7 +43,7 @@ pub fn apps(
apps_service: State<AppsService>,
request_info: RequestInfo,
) -> Result<Json<MultiMap<String, Service>>, HttpApiProblem> {
let mut apps = apps_service.get_apps()?;
let mut apps = apps_service.get_apps(&request_info)?;

for (_, services) in apps.iter_all_mut() {
for service in services.iter_mut() {
@@ -26,6 +26,8 @@

#![feature(custom_attribute, proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate clap;
#[macro_use]
extern crate failure;
#[macro_use]
@@ -28,3 +28,4 @@ pub mod request_info;
pub mod service;
pub mod ticket_info;
pub mod web_hook_info;
pub mod web_host_meta;
@@ -34,6 +34,33 @@ pub struct RequestInfo {
}

impl RequestInfo {
#[cfg(test)]
pub fn new(base_url: Url) -> Self {
RequestInfo { base_url }
}

/// Returns the value for the `host` value of the
/// [Forwarded](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded) header.
pub fn host(&self) -> String {
match self.base_url.scheme() {
"http" => match self.base_url.port() {
None | Some(80) => String::from(self.base_url.host_str().unwrap()),
Some(port) => format!("{}:{}", self.base_url.host_str().unwrap(), port),
},
"https" => match self.base_url.port() {
None | Some(443) => String::from(self.base_url.host_str().unwrap()),
Some(port) => format!("{}:{}", self.base_url.host_str().unwrap(), port),
},
_ => String::from(self.base_url.host_str().unwrap()),
}
}

/// Returns the value for the `proto` value of the
/// [Forwarded](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded) header.
pub fn scheme(&self) -> &str {
self.base_url.scheme()
}

pub fn get_base_url(&self) -> &Url {
&self.base_url
}
@@ -24,10 +24,13 @@
* =========================LICENSE_END==================================
*/

use crate::models::web_host_meta::WebHostMeta;
use chrono::{DateTime, Utc};
use regex::Regex;
use serde::ser::{Serialize, Serializer};
use serde::{de, Deserialize, Deserializer};
use std::collections::BTreeMap;
use std::net::IpAddr;
use std::str::FromStr;
use url::Url;

@@ -36,8 +39,25 @@ pub struct Service {
app_name: String,
service_name: String,
container_type: ContainerType,
container_id: String,
base_url: Option<Url>,
endpoint: Option<ServiceEndpoint>,
web_host_meta: Option<WebHostMeta>,
}

#[derive(Clone, Debug)]
struct ServiceEndpoint {
internal_addr: IpAddr,
exposed_port: u16,
}

impl ServiceEndpoint {
fn to_url(&self) -> Url {
Url::parse(&format!(
"http://{}:{}/",
self.internal_addr, self.exposed_port
))
.unwrap()
}
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq)]
@@ -146,18 +166,14 @@ impl ServiceConfig {
}

impl Service {
pub fn new(
app_name: String,
service_name: String,
container_id: String,
container_type: ContainerType,
) -> Service {
pub fn new(app_name: String, service_name: String, container_type: ContainerType) -> Service {
Service {
app_name,
service_name,
container_id,
container_type,
base_url: None,
endpoint: None,
web_host_meta: None,
}
}

@@ -184,13 +200,34 @@ impl Service {
&self.service_name
}

pub fn container_id(&self) -> &String {
&self.container_id
}

pub fn container_type(&self) -> &ContainerType {
&self.container_type
}

pub fn port(&self) -> Option<u16> {
match &self.endpoint {
None => None,
Some(endpoint) => Some(endpoint.exposed_port),
}
}

pub fn set_endpoint(&mut self, addr: IpAddr, port: u16) {
self.endpoint = Some(ServiceEndpoint {
internal_addr: addr,
exposed_port: port,
})
}

pub fn endpoint_url(&self) -> Option<Url> {
match &self.endpoint {
None => None,
Some(endpoint) => Some(endpoint.to_url()),
}
}

pub fn set_web_host_meta(&mut self, meta: Option<WebHostMeta>) {
self.web_host_meta = meta;
}
}

impl Serialize for Service {
@@ -205,23 +242,39 @@ impl Serialize for Service {
url: Option<String>,
#[serde(rename = "type")]
service_type: String,
version_url: Option<String>,
version: Option<Version>,
open_api_url: Option<String>,
}

let version_url = match self.container_type {
ContainerType::Instance | ContainerType::Replica => self
.service_url()
// TODO: use Web Host Metadata (RFC 6415): .well-known/host-meta.json
.map(|url| url.join("version").unwrap())
.map(|url| url.to_string()),
_ => None,
};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Version {
git_commit: Option<String>,
software_version: Option<String>,
date_modified: Option<DateTime<Utc>>,
}

let software_version = self.web_host_meta.clone().and_then(|meta| meta.version());
let open_api_url = self.web_host_meta.clone().and_then(|meta| meta.openapi());
let git_commit = self.web_host_meta.clone().and_then(|meta| meta.commit());
let date_modified = self
.web_host_meta
.clone()
.and_then(|meta| meta.date_modified());

let s = Service {
name: &self.service_name,
url: self.service_url().map(|url| url.to_string()),
url: match self.web_host_meta {
None => None,
Some(_) => self.service_url().map(|url| url.to_string()),
},
service_type: self.container_type.to_string(),
version_url,
version: Some(Version {
git_commit,
software_version,
date_modified,
}),
open_api_url,
};

Ok(s.serialize(serializer)?)
Oops, something went wrong.

0 comments on commit 508952d

Please sign in to comment.
You can’t perform that action at this time.