Spring Cloud Canary Deployment - spring-boot

I have a spring cloud micro service with Zuul running on docker.
Requirement:
I want to create canary deployment with specific requirement as we will have x clients and I want to canary test with y specific clients (using email or username).
Can I configure the gateway to route requests to the new version of the micro-service for these y clients?

So you can do that via configuration or dynamic routing but i think first idom is not good for generic part every client you have to define it again and again but second one is more good
#Component
public class PostFilter extends ZuulFilter {
private static final String REQUEST_PATH = "/special-customer-product-request-url";
private static final String TARGET_SERVICE = "special-customer-service";
private static final String HTTP_METHOD = "POST or GET";
private final DiscoveryClient discoveryClient;
public PostOrdersFilter(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
#Override
public String filterType() {
return "route";
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String method = request.getMethod();
String requestURI = request.getRequestURI();
return HTTP_METHOD.equalsIgnoreCase(method) && requestURI.startsWith(REQUEST_PATH);
}
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
List<ServiceInstance> instances = discoveryClient.getInstances(TARGET_SERVICE);
try {
if (instances != null && instances.size() > 0) {
context.setRouteHost(instances.get(0).getUri().toURL());
} else {
throw new IllegalStateException("Target service instance not found!");
}
} catch (Exception e) {
throw new IllegalArgumentException("Couldn't get service URL!", e);
}
return null;
}
}

Related

Spring Boot KeyCloak Override ClientIdAndSecretCredentialsProvider not working

I am trying to override ClientIdAndSecretCredentialsProvider in KeyCloak Spring Boot. Below is the code I have tried.
public class CustomClientCredentialsProvider extends ClientIdAndSecretCredentialsProvider {
public static final String PROVIDER_ID = CredentialRepresentation.SECRET;
private String clientSecret;
#Override
public String getId() {
return PROVIDER_ID;
}
#Override
public void init(KeycloakDeployment deployment, Object config) {
clientSecret = (String) config;
}
#Override
public void setClientCredentials(KeycloakDeployment deployment, Map<String, String> requestHeaders, Map<String, String> formParams) {
String clientId = deployment.getResourceName();
if (!deployment.isPublicClient()) {
if (clientSecret != null) {
// do something else
}
} else {
formParams.put(OAuth2Constants.CLIENT_ID, clientId);
}
}
}
However even after overwrite, I can still see the control going to the existing Spring Boot's ClientIdAndSecretCredentialsProvider rather than mine.
How can I get the control to mine rather than Spring Boot's? Is there something else that needs to be set??
I believe you have to pass an instance of CustomClientCredentialsProvider to the Adapter Deployment.
See AdapterDeploymentContext#setClientAuthenticator(ClientCredentialsProvider clientAuthenticator)

Show a static HTML page while the Spring context initializes

Spring Boot 2.3.7, Embedded Jetty, JSP and WAR packaging. I want to show my some static HTML page while spring context initializes. It should be visible when application starts and before spring context refreshed. I tried to use this manual as example https://www.nurkiewicz.com/2015/09/displaying-progress-of-spring.html but this doesn't work.
I need to start embedded jetty directly when jetty is initialized. But spring boot starts embedded jetty only when context refreshed.
How should I do this?
I created a Jetty warpper warmup class:
public final class WarmupServer {
private final String contextPath;
private final String displayName;
private final DefaultApplicationArguments arguments;
private final String[] welcomeFiles;
private final Resource baseResource;
private Server server;
public WarmupServer(String contextPath,
String displayName,
Resource baseResource,
String[] welcomeFiles,
String... runArgs) {
this.contextPath = StringUtils.defaultIfBlank(contextPath, "/");
this.displayName = StringUtils.defaultIfBlank(displayName, "Warmup");
this.baseResource = ObjectUtils.defaultIfNull(baseResource, Resource.newClassPathResource("/static"));
this.welcomeFiles = ArrayUtils.isEmpty(welcomeFiles) ? new String[]{"html/warmup.html"} : welcomeFiles;
this.arguments = new DefaultApplicationArguments(ArrayUtils.nullToEmpty(runArgs));
}
public Server start() throws Exception {
if (server != null && server.isStarted()) {
throw new IllegalStateException("Server already started");
}
server = new Server();
server.setHandler(buildServletHandler());
final String configPath = parseArg(OPT_CONFIG);
if (StringUtils.isBlank(configPath)) {
throw new RuntimeException(OPT_CONFIG + " argument is not set");
}
final Config config = ConfigUtils.parseFile(new File(configPath), DEFAULT_CONFIG_FILE_NAME);
configureHttpConnector(config);
configureHttpsConnector(config);
server.start();
return server;
}
public void registerWarmupServerStopLifecycle(ConfigurableApplicationContext context) {
context.getBeanFactory()
.registerSingleton(WarmupStopLifecycle.class.getSimpleName(), new WarmupStopLifecycle(server));
}
private ServletContextHandler buildServletHandler() {
final ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.SESSIONS);
handler.addServlet(DefaultServlet.class, "/");
handler.setDisplayName(displayName);
handler.setContextPath(contextPath);
handler.setWelcomeFiles(welcomeFiles);
handler.setBaseResource(baseResource);
return handler;
}
private void configureHttpConnector(Config config) {
final int httpPort = NumberUtils.toInt(parseArg(OPT_HTTP_PORT), config.getInt(OPT_HTTP_PORT));
final ServerConnector connector = new ServerConnector(server);
connector.setPort(httpPort);
server.addConnector(connector);
}
private void configureHttpsConnector(Config config) {
final int httpsPort = NumberUtils.toInt(
parseArg(OPT_HTTPS_PORT), config.getInt(OPT_HTTPS_PORT));
final String keyStorePath = StringUtils.defaultIfBlank(
parseArg(OPT_KEYSTORE_FILE), config.getString(OPT_KEYSTORE_FILE));
final boolean sslEnabled = StringUtils.isNotBlank(keyStorePath)
&& Files.isReadable(Paths.get(keyStorePath));
if (sslEnabled) {
final HttpConfiguration configuration = new HttpConfiguration();
configuration.setSecurePort(httpsPort);
configuration.setSecureScheme(HTTPS_SCHEME);
final ServerConnector httpsConnector = new HttpsConnector()
.createConnector(server, configuration, config.getConfig(JETTY_HTTPS_CONFIG), httpsPort);
server.addConnector(httpsConnector);
}
}
private String parseArg(String optionName) {
final List<String> values = arguments.getOptionValues(optionName);
return CollectionUtils.isEmpty(values) ? StringUtils.EMPTY : values.get(0);
}
public static WarmupServer start(String contextPath,
String displayName,
Resource baseResource,
String[] welcomeFiles,
String... runArgs) throws Exception {
final WarmupServer server = new WarmupServer(contextPath, displayName, baseResource, welcomeFiles, runArgs);
server.start();
return server;
}
}
This wrapper parses command line arguments and creates a Jetty handler and HTTP and (or) HTTPS connectors by using provided command line arguments.
And the simple Spring's Lifecycle implementation class:
#RequiredArgsConstructor
class WarmupStopLifecycle implements SmartLifecycle {
private static final Logger logger = LogManager.getFormatterLogger();
private final Server warmupServer;
private volatile boolean isRunning;
#Override
public void start() {
try {
warmupServer.stop();
isRunning = true;
} catch (Exception e) {
logger.error("Failed to stop warmup server", e);
throw new RuntimeException("Failed to stop warmup server", e);
}
}
#Override
public void stop() {
}
#Override
public boolean isRunning() {
return isRunning;
}
/**
* Returns phase of this lifecycle.
* A phase MUST be before the Spring web server starts.
* See {#code org.springframework.boot.web.servlet.context.WebServerStartStopLifecycle} phase.
*/
#Override
public int getPhase() {
return Integer.MAX_VALUE - 2;
}
}
So usage of this:
#SpringBootApplication
public class SpringApplication {
public static void main(String[] args) throws Exception {
final WarmupServer warmupServer = WarmupServer.start(
"/my_context_path", "My Warmup server handler", args);
new SpringApplicationBuilder()
.sources(SpringApplication.class)
.initializers(warmupServer::registerWarmupServerStopLifecycle)
.run(args);
}
}
WarmupServer starts immediately after the application runs and will be stopped before starting the Spring's web server.

No thread-bound request found with Spring when Kafka receive a message

I 'm getting this error from my service
jvm org.hibernate.internal.ExceptionMapperStandardImpl {"#trace_info":"[availability-psql,eba16d49e23479cc,675789f41e0dda5b,eba16d49e23479cc,false]", "#message": "HHH000346: Error during managed flush [Error creating bean with name 'scopedTarget.infoUser': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.]
This is because of I have a bean of scope #ScopeRequest. This problem show up when a new message from kafka is received and I try to update my data base with spring data. If I remove my #Transactional I don't have any problem to save the data.
#KafkaListener(topics = "#{kafkaMastersConfig.topics}", containerFactory = "mastersContainerFactory")
#Transactional
#Authorized
public void consumeWrapperMasterChangeEvent(#Payload String payload,
#Header(KafkaHeaders.RECEIVED_TOPIC) String topic, #Nullable #Header(AUTHORIZATION) String authorization) throws IOException {
try {
log.info("Received change event in masters: '{}'", payload);
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
RequestContextHolder.setRequestAttributes(context);
changeProcessorFactory.getEntityChangeProcessor(getEntityFromTopic(topic)).processChange(payload);
} catch ( Exception e ) {
log.error("Error proccesing message {} ", e.getMessage());
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
And here is the bean:
#RequestScope
#Component
#NoArgsConstructor
#Getter
#Setter
public class InfoUser {
private DecodedJWT jwt;
public String getCurrentUser() {
if (jwt == null) {
return null;
}
return jwt.getSubject();
}
public String getAuthorizationBearer() {
if (jwt == null) {
return null;
}
return jwt.getToken();
}
}
And this class:
public class CustomRequestScopeAttr implements RequestAttributes {
private Map<String, Object> requestAttributeMap = new HashMap<>();
#Override
public Object getAttribute(String name, int scope) {
if (scope == RequestAttributes.SCOPE_REQUEST) {
return this.requestAttributeMap.get(name);
}
return null;
}
#Override
public void setAttribute(String name, Object value, int scope) {
if (scope == RequestAttributes.SCOPE_REQUEST) {
this.requestAttributeMap.put(name, value);
}
}
#Override
public void removeAttribute(String name, int scope) {
if (scope == RequestAttributes.SCOPE_REQUEST) {
this.requestAttributeMap.remove(name);
}
}
#Override
public String[] getAttributeNames(int scope) {
if (scope == RequestAttributes.SCOPE_REQUEST) {
return this.requestAttributeMap.keySet().toArray(new String[0]);
}
return new String[0];
}
#Override
public void registerDestructionCallback(String name, Runnable callback, int scope) {
// Not Supported
}
#Override
public Object resolveReference(String key) {
// Not supported
return null;
}
#Override
public String getSessionId() {
return null;
}
#Override
public Object getSessionMutex() {
return null;
}
}
And futhermore I have an aspect class to save the authorization token:
#Aspect
#Component
#RequiredArgsConstructor
public class AuthorizationAspect {
private final AuthorizationDecoder authorizationDecoder;
private final ApplicationContext applicationContext;
#Around("#annotation(Authorized)")
public Object setInfoUser(ProceedingJoinPoint joinPoint) throws Throwable {
try {
String[] parameterNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Object[] args = joinPoint.getArgs();
Map<String, Object> arguments = new HashMap<>();
for (int i = 0; i < args.length; i++) {
if (null != args[i]) {
arguments.put(parameterNames[i], args[i]);
}
}
Object authorization = arguments.get("authorization");
RequestContextHolder.setRequestAttributes(new CustomRequestScopeAttr());
InfoUser infoUser = applicationContext.getBean(InfoUser.class);
infoUser.setJwt(authorizationDecoder.decodeToken((String) authorization));
return joinPoint.proceed();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
And the last class is trying to save de info:
#RequiredArgsConstructor
public class RoomChangeMaster implements ChangeMaster<Room> {
private final TimetableRepository timetableRepository;
private final AvailabilityRepository availabilityRepository;
#Override
public void processChange(Room entity, ActionEnum action) {
if (action == ActionEnum.updated) {
List<Timetable> timetables = (List<Timetable>) timetableRepository.findByRoomId(entity.getId());
Room room = timetables.get(0).getRoom();
room.setDescription(entity.getDescription());
room.setCode(entity.getCode());
timetables.forEach(timetable -> {
timetable.setRoom(room);
timetableRepository.save(timetable);
});
availabilityRepository
.updateAvailabilityRoomByRoomId(room, entity.getId());
} else {
throw new IllegalStateException("Unexpected value: " + action);
}
}
}
I have spent a lot of time finding out the problem, but so far, I was not able to know the problem. Any idea will be appreciate.
Thank you
RequestContextHolder is for Spring-MVC - it is for a Web request only and is populated with information from an HTTP request.
/**
* Holder class to expose the web request in the form of a thread-bound
* {#link RequestAttributes} object. The request will be inherited
* by any child threads spawned by the current thread if the
* {#code inheritable} flag is set to {#code true}.
*
...
There is no equivalent for listener containers (of any type) because there is no "incoming request".
Looks like your hibernate code is tightly tied to the web.
If you are trying to reuse existing code you need to decouple it and use some other technique to pass information between the layers (e.g. a custom equivalent of RequestContextHolder).
Finally, I have solved it changing the hiberante method save by saveAndFlush

How i can pass new Object between Zuul filters, using spring context?

Currently, I have AuthFilter and here I received an UserState. I need to pass it to the next Filter. But how to do it right? Or exists other practices to resolve it?
public class AuthFilter extends ZuulFilter {
#Autowired
private AuthService authService;
#Autowired
private ApplicationContext appContext;
#Override
public String filterType() {
return PRE_TYPE;
}
#Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 2;
}
#Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
String requestURI = context.getRequest().getRequestURI();
for (String authPath : authPaths) {
if (requestURI.contains(authPath)) return true;
}
return false;
}
#Override
public Object run() throws ZuulException {
try {
UserState userState = authService.getUserData();
DefaultListableBeanFactory context = new DefaultListableBeanFactory();
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(UserState.class);
beanDefinition.setPropertyValues(new MutablePropertyValues() {
{
add("user", userState);
}
});
context.registerBeanDefinition("userState", beanDefinition);
} catch (UndeclaredThrowableException e) {
if (e.getUndeclaredThrowable().getClass() == UnauthorizedException.class) {
throw new UnauthorizedException(e.getMessage());
}
if (e.getUndeclaredThrowable().getClass() == ForbiddenException.class) {
throw new ForbiddenException(e.getMessage(), "The user is not allowed to make this request");
}
}
return null;
}
}
I pretty sure filters are chained together and the request/response are passed through them. You can add the data to the request, and have the next filter look for it.

How to make Zuul dynamic routing based on HTTP method and resolve target host by 'serviceId'?

How to make Zuul dynamic routing based on HTTP method (GET/POST/PUT...)?
For example, when you need to route the POST request to the different host instead of the default one described in 'zuul.routes.*'...
zuul:
routes:
first-service:
path: /first/**
serviceId: first-service
stripPrefix: false
second-service:
path: /second/**
serviceId: second-service
stripPrefix: false
I.e. when we request 'GET /first' then Zuul route the request to the 'first-service', but if we request 'POST /first' then Zuul route the request to the 'second-service'.
To implement dynamic routing based on HTTP method we can create a custom 'route' type ZuulFilter and resolve 'serviceId' through DiscoveryClient. Fore example:
#Component
public class PostFilter extends ZuulFilter {
private static final String REQUEST_PATH = "/first";
private static final String TARGET_SERVICE = "second-service";
private static final String HTTP_METHOD = "POST";
private final DiscoveryClient discoveryClient;
public PostOrdersFilter(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
#Override
public String filterType() {
return "route";
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String method = request.getMethod();
String requestURI = request.getRequestURI();
return HTTP_METHOD.equalsIgnoreCase(method) && requestURI.startsWith(REQUEST_PATH);
}
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
List<ServiceInstance> instances = discoveryClient.getInstances(TARGET_SERVICE);
try {
if (instances != null && instances.size() > 0) {
context.setRouteHost(instances.get(0).getUri().toURL());
} else {
throw new IllegalStateException("Target service instance not found!");
}
} catch (Exception e) {
throw new IllegalArgumentException("Couldn't get service URL!", e);
}
return null;
}
}
#Cepr0's solution is right. Here I am proposing just a simpler way (without service discovery). Assuming you have that route:
zuul:
routes:
first:
path: /first/**
# No need for service id or url
Then you can route requests for '/first' route in 'route' type filter just by setting location to request context.
#Component
public class RoutingFilter extends ZuulFilter {
#Override
public String filterType() {
return ROUTE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() throws ZuulException {
/* Routing logic goes here */
URL location = getRightLocationForRequest();
ctx.setRouteHost(location);
return null;
}
}

Resources