Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API Gateway #1572

Merged
merged 8 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions api-gateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# API Gateway for VRO

To support serving up APIs implemented in several languages (e.g., Java by the RRD Team and Python used by the CC Team),
this API Gateway acts as a proxy to forward requests to the specified tenant API, as determined by the URI prefix.

The URI prefixes are configured in `application.yml` -- under `spring.cloud.gateway.routes`.

The Swagger UI destinations to tenant APIs are configured in `application.yml` -- under `springdoc.swagger-ui.urls`.

The implementation is based on https://piotrminkowski.com/2020/02/20/microservices-api-documentation-with-springdoc-openapi/,
which uses Spring Boot 3 and Spring Cloud Gateway.
24 changes: 24 additions & 0 deletions api-gateway/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
id 'local.std.java.library-spring-conventions'
id 'local.java.container-spring-conventions'
id "com.google.osdetector" version "1.7.3"
}

dependencies {
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// Spring Cloud
implementation 'org.springframework.cloud:spring-cloud-starter-gateway:4.0.5'
if (osdetector.classifier == "osx-aarch_64") {
// Fix MacOS error: Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider,
// which may result in incorrect DNS resolutions.
// Spring Cloud Gateway uses Netty
runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.92.Final:${osdetector.classifier}")
}

// Swagger UI for WebFlux
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.1.0'
}
1 change: 1 addition & 0 deletions api-gateway/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
spring_boot_version=3.0.6
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Spring Boot 3. The rest of VRO is using Spring Boot 2.

18 changes: 18 additions & 0 deletions api-gateway/src/main/java/gov/va/vro/GatewayApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gov.va.vro;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@Slf4j
@SpringBootApplication
@ConfigurationPropertiesScan(basePackages = {"gov.va.vro"})
public class GatewayApplication {
public static void main(String[] args) {
new SpringApplication(GatewayApplication.class).run(args);
log.info("\n-------- API Gateway Application Started ---------");
}
}
14 changes: 14 additions & 0 deletions api-gateway/src/main/java/gov/va/vro/GatewayConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package gov.va.vro;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayConfiguration {
@Bean
HomePageModel homePageModel(){
HomePageModel homePageModel = new HomePageModel();
return homePageModel;
}
}
18 changes: 18 additions & 0 deletions api-gateway/src/main/java/gov/va/vro/HomePageController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gov.va.vro;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@RequiredArgsConstructor
class HomePageController {
final HomePageModel homePageModel;

@GetMapping("/")
String index(final Model model) {
model.addAttribute("model", homePageModel);
return "index";
}
}
10 changes: 10 additions & 0 deletions api-gateway/src/main/java/gov/va/vro/HomePageModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gov.va.vro;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;

@Data
class HomePageModel {
@Value("${vro.openapi.info.version}")
String version;
}
47 changes: 47 additions & 0 deletions api-gateway/src/main/java/gov/va/vro/OpenApiConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package gov.va.vro;

import gov.va.vro.propmodel.Info;
import gov.va.vro.propmodel.OpenApiProperties;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class OpenApiConfiguration {
private final OpenApiProperties openApiProperties;

@Bean
public OpenAPI customOpenApi() {
Info info = openApiProperties.getInfo();
gov.va.vro.propmodel.Contact contact = info.getContact();
gov.va.vro.propmodel.License license = info.getLicense();

List<Server> servers =
openApiProperties.getServers().stream()
.map(server -> new Server().description(server.getDescription()).url(server.getUrl()))
.collect(Collectors.toList());

OpenAPI config = new OpenAPI()
.info(
new io.swagger.v3.oas.models.info.Info()
.title(info.getTitle())
.description(info.getDescription())
.version(info.getVersion())
.license(new License().name(license.getName()).url(license.getUrl()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to not just pass existing contact and license objects? It would make the code simpler.

Copy link
Contributor Author

@yoomlam yoomlam May 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're different object types:

  • io.swagger.v3.oas.models.info.Contact - what is expected
  • gov.va.vro.propmodel.Contact - what we create as a result of parsing the application.yaml file

.contact(new Contact().name(contact.getName()).email(contact.getEmail())))
.servers(servers);
return config;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package gov.va.vro.config.propmodel;
package gov.va.vro.propmodel;

import lombok.Getter;
import lombok.Setter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package gov.va.vro.config.propmodel;
package gov.va.vro.propmodel;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Info {
private String title = "VRO API";
private String description = "VRO Description";
private String version = "v1.0.25";
private String title = "API";
private String description = "Description";
private String version = "v0.0.0";

private final Contact contact = new Contact();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package gov.va.vro.config.propmodel;
package gov.va.vro.propmodel;

import lombok.Getter;
import lombok.Setter;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package gov.va.vro.propmodel;

import gov.va.vro.propmodel.Info;
import gov.va.vro.propmodel.Server;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Getter
@Setter
@ConfigurationProperties(prefix = "vro.openapi")
public class OpenApiProperties {
private final Info info = new Info();

private List<Server> servers = new ArrayList<Server>(Arrays.asList(new Server()));
}
11 changes: 11 additions & 0 deletions api-gateway/src/main/java/gov/va/vro/propmodel/Server.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gov.va.vro.propmodel;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Server {
private String description = "Local Server";
private String url = "/";
}
121 changes: 121 additions & 0 deletions api-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# This file contains shared properties across all environments; it is always loaded by Spring
# See https://github.com/department-of-veterans-affairs/abd-vro/wiki/Configuration-settings#vros-use-of-spring-profiles

management:
endpoints.web:
exposure.include: "*"
endpoint:
health:
show-details: always
probes:
enabled: true
group:
liveness.include: livenessState
readiness.include: readinessState, db
metrics:
enabled: true
distribution:
percentiles.http.server.requests: 0.5, 0.90, 0.95, 0.99, 0.999
percentiles-histogram.http.server.requests: true
sla.http.server.requests: 10ms, 50ms
slo.http.server.requests: 10ms, 50ms
tags:
group: starter
service: example
region: "${POD_REGION:local}"
stack: "${CLUSTER:dev}"
ns: "${NAMESPACE:example}"
pod: "${POD_ID:docker}"
web.server.request.autotime.enabled: true
server.port: 8061

server:
ssl:
enabled: false
port: 8060
maxHttpHeaderSize: 48000
session:
timeout: 60
connection:
timeout: 60000
servlet:
session:
timeout: 120000

spring:
application:
name: "vro-api"
servlet:
multipart:
maxFileSize: 25MB
maxRequestSize: 25MB
enabled: true
session:
timeout: 120000
resources:
add-mappings: false
http:
encoding:
force: true

springdoc:
writer-with-default-pretty-printer: true
show-actuator: true
swagger-ui:
# http://localhost:8078/swaggerui will be forwarded to http://localhost:8078/webjars/swagger-ui/index.html
path: /swaggerui
operations-sorter: method
tagsSorter: alpha
# Populate API dropdown on the upper-right of Swagger UI
urls:
- name: 0. Gateway API
# API defined for this API Gateway
url: /v3/api-docs
- name: App API
# API defined for the VRO Java-base App
url: /app/v3/api-docs
- name: Contention Classification API
# Use the route defined under spring.cloud.gateway below
url: /contention-classification/openapi.json
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lukey-luke If you the location of the openapi.json changes, please update this line.


# TODO: Add Spring Cloud CircuitBreaker: https://spring.io/guides/gs/gateway/
spring.cloud.gateway:
actuator.verbose.enabled: true
discovery:
locator:
enabled: false
routes:
# Each domain API should have one entry with a URI prefix
- id: vro-app
uri: http://localhost:8080
predicates:
- Path=/app/**
filters:
- RewritePath=/app/(?<path>.*), /$\{path}
- id: contention-classification-tenant
# TODO: add Swagger servers such that /contention-classification is used as the prefix for requests
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uri: http://localhost:18000
predicates:
- Path=/contention-classification/**
filters:
- RewritePath=/contention-classification/(?<path>.*), /$\{path}

log4j2:
formatMsgNoLookups: true

vro:
openapi:
info:
title: "Automated Benefits Delivery (ABD): Virtual Regional Office (VRO) API"
description: "To improve benefit delivery to Veterans"
version: "3.0.0"
contact:
name: Premal Shah
email: "[email protected]"
license:
name: CCO 1.0
url: "https://github.com/department-of-veterans-affairs/abd-vro/blob/master/LICENSE.md"
servers:
-
description: "Server"
url: ${STARTER_OPENAPI_SERVERURL:/}
24 changes: 24 additions & 0 deletions api-gateway/src/main/resources/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<html>
<body style="font-family: Arial">
<h1>VRO API Gateway</h1>
Automated Benefits Delivery - Virtual Regional Office (ABD-VRO)
<p><a href="https://github.com/department-of-veterans-affairs/abd-vro">ABD-VRO GitHub Repo</a>
</p>

<h3>Swagger UI</h3>
APIs for each VRO tenant is available via VRO's <a href="webjars/swagger-ui/index.html">Swagger UI</a>.
<ul>
<li>Select the tenant from the "Select a definition" dropdown on the upper-right of the UI.</li>
<li>Each tenant page has a link to the API spec.</li>
<li>To use Swagger's "Try It" feature, make sure to select the correct server on the left dropdown.
For example, for the VRO App, select the "/app - via API Gateway" server.
</li>
</ul>

<h3>VRO info</h3>
<ul>
<li>VRO version: <span th:text="${model.version}" /></li>
</ul>

</body>
</html>
10 changes: 0 additions & 10 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ dependencies {
implementation project(':svc-bip-api')

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-amqp'
implementation "com.vladmihalcea:hibernate-types-55:${hibernate_types_version}"
implementation "org.springframework.security:spring-security-core:${spring_security_version}"
implementation "org.springframework.security:spring-security-config:${spring_security_version}"
implementation "org.springframework.security:spring-security-web:${spring_security_version}"
Expand All @@ -39,14 +36,7 @@ dependencies {
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.14'
implementation 'io.jsonwebtoken:jjwt:0.2'

// Needed?
// implementation "org.postgresql:postgresql:${postgresql_version}"
runtimeOnly "org.postgresql:postgresql:${postgresql_version}"

testRuntimeOnly "com.h2database:h2:${h2_version}"

testImplementation "org.apache.camel:camel-test-spring-junit5:${camel_version}"
testImplementation "io.jsonwebtoken:jjwt:0.2"
}

openApi {
Expand Down
Loading