Using Camel, CDI Inside Kubernetes With Fabric8
Learn about how to integrate Apache Camel and Fabric8 into an existing Kubernetes CDI service.
Join the DZone community and get the full member experience.
Join For FreeI recently blogged about Injecting Kubernetes Services with CDI. In this post I am going to take things one step further and bring Apache Camel into the picture. So, I am going to use Camel's CDI support to wire my components and routes, along with Fabric8's CDI extension to automatically inject Kubernetes services into my components.
I am going to reuse stuff from my previous post (so give it a read if u haven't already) to build an standalone camel cdi application that is going to expose the contents of a database via http (a simple http to jdbc and back again). Everything will run in Docker and orchestration will be done by Kubernetes.
So first thing first. How camel and cdi works....
The camel cdi registry
Apache Camel is using the notion of a registry. It uses the registry to lookup for objects, that are needed by the routes. Those lookups may by type or by name.
The most common use of the registry is when the endpoint uri is processed, camel will parse the scheme and will lookup the registry by name for the appropriate component. Other cases involve passing bean references to endpoints by name and so on...
In other words Apache Camel may perform lookups on the bean registry on runtime.
Any extension that needs to play nicely with Apache Camel needs to provide beans with a predictable names.
The @Alias annotation
Fabric8's CDI extension, for any given service, may register more than one beans (one per service per type, per protocol ...). So, it's impossible to have service beans named after the service. Also the user shouldn't have to memorise the naming conventions that are used internally...
"So, how does Fabric8 play with frameworks that rely on 'by name' lookups?"
Fabric8 provides the @Alias annotation which allows the developer to explicitly specify the bean name of the injected service. Here's an example:
import javax.inject.Inject;
import io.fabric8.annotations.Protocol;
import io.fabric8.annotations.ServiceName;
public class MysqlExampleWithAlias {
public MysqlExampleWithAlias(@Inject @Alias("mysqldb") @ServiceName("mysql") String serivceUrl) {
System.out.println("Bean Name: mysqldb. Type: String. Value:"+serviceUrl);
}
}
"What happens here?"
The Fabric8 cdi extension will receive an event that there is an injection point of type String, with 2 qualifiers:
- ServiceName with value "mysql".
- Alias with value "mysqldb".
So when it creates beans and producers for that service it will use the "mysqldb" as a name. This is what allows control over the Fabric8 managed beans and makes name lookups possible.
Using @Factory to create or configure Camel components or endpoints
In my previous post, I went through some examples on how you could use Fabric8's @Factory annotation in order to create jdbc connections. Now, I am going to create a factory for a jdbc datasource, which then is going to be added to the Apache Camel Cdi Bean Registry.
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import io.fabric8.annotations.Configuration;
import io.fabric8.annotations.Factory;
import io.fabric8.annotations.ServiceName;
import javax.sql.DataSource;
public class DatasourceFactory {
private static final String TCP_PROTO = "tcp";
private static final String JDBC_PROTO = "jdbc:mysql";
@Factory
@ServiceName
public DataSource create(@ServiceName String url, @Configuration MysqlConfiguration conf) {
MysqlDataSource ds = new MysqlDataSource();
ds.setURL(url.replaceFirst(TCP_PROTO, JDBC_PROTO) + "/" + conf.getDatabaseName());
ds.setUser(conf.getUsername());
ds.setPassword(conf.getPassword());
return ds;
}
Now if we wanted to refer this datasource from an Apache Camel endpoint, we would have to specify the "name" of the datasource to the endpoint uri. For example "jdbc:custmersds", where customersds is the name of the datasource.
"But, how can I name the fabric8 managed datasource?"
This is how the @Alias saves the day:
import io.fabric8.annotations.Alias;
import io.fabric8.annotations.ServiceName;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.cdi.ContextName;
import org.apache.camel.model.language.ConstantExpression;
import javax.ejb.Startup;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.sql.DataSource;
@ContextName("myCdiCamelContext")
@Startup
@ApplicationScoped
public class MyRoutes extends RouteBuilder {
@Inject
@ServiceName("mysql-service")
@Alias("customerds")
DataSource dataSource;
@Override
public void configure() throws Exception {
from("jetty:http://0.0.0.0:8080/list/")
.setBody(new ConstantExpression("select * from customers"))
.to("jdbc:customerds");
}
}
This is a typical RouteBuilder for CDI based Camel application. What is special about it is that we inject a DataSource named "customersds".
"Who provides the DataSource?"
Short answer: Fabric8.
Not so short answer: The @ServiceName("mysql") annotation tells Fabric8 that the DataSource refers to the "mysql" Kubernetes service. Fabric8 will obtain the url to that service for us. Since the type of the field is neither String, nor URL but DataSource, Fabric8 will lookup for @Factory methods that are capable of converting a String to a DataSource. In our case it will find the DataSourceFactory class which does exactly that. As this was not awesome enough the DataSourceFactory also accepts @Configuration MysqlConfiguration, so that we can specify things like database name, credentials etc (see my previous post).
Configuring the DataSource
Before I start explaining how we can configure the DataSource, let me take one step back and recall MysqlConfiguration from my previous post:
import org.apache.deltaspike.core.api.config.ConfigProperty;
import javax.inject.Inject;
public class MysqlConfiguration {
@Inject
@ConfigProperty(name = "USERNAME", defaultValue = "admin")
private String username;
@Inject
@ConfigProperty(name = "PASSWORD", defaultValue = "admin")
private String password;
@Inject
@ConfigProperty(name = "DATABASE_NAME", defaultValue = "mydb")
private String databaseName;
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getDatabaseName() {
return databaseName;
}
}
As I mentioned in my previous post we can use environment variables in order to pass configuration to our app. Remember this app is intended to live inside a Docker container....
MysqlConfiguration contains 3 fields:
- Field username for environment variable USERNAME
- Field password for environment variable PASSWORD
- Field databseName for environmnet variable DATABASE_NAME
So we need 3 environment variables one for each fields. Then our DataSourceFactory will be passed an instance of MysqlConfiguration with whatever values can be retrieved from the environment, so that it create the actual DataSource.
"But how could I reuse MysqlConfiguration to configure multiple different services ?"
So, the idea is that a @Factory and a @Configuration can be reusable. After all no need to have factories and model classes bound to the underlying services, right?
Fabric8 helps by using the service name as a prefix for the environment variables. It does that on runtime and it works like this:
- The Fabric8 extension discovers an Injection Point annotated with @ServiceName
- It will check the target type and it will lookup for a @Factory if needed.
- The @Factory accepts the service URL and an instance MysqlConfiguration
- MysqlConfiguration will be instantiated using the value of @ServiceName as an environment variable prefix.
So for our example to work we would need to package our application as a Docker container and then use the following Kubernetes configuration:
{
"image": "camel-cdi-jdbc",
"imagePullPolicy": "IfNotPresent",
"name": "camel-cdi-jdbc",
"env": [
{
"name": "MYSQL_SERVICE_USERNAME",
"value": "admin"
},
{
"name": "MYSQL_SERVICE_PASSWORD",
"value": "password"
},
{
"name": "MYSQL_SERVICE_DATABASE_NAME",
"value": "customers"
}
]
}
Now if we need to create an additional DataSource (say for a jdbc to jdbc bridge) inside the same container, we would have to just specify additional environment variable for the additional Kubernetes. Now, if the name of the service was "mysql-target", then our Kubernetes configuration would need to look like:
{
"image": "camel-cdi-jdbc",
"imagePullPolicy": "IfNotPresent",
"name": "camel-cdi-jdbc",
"env": [
{
"name": "MYSQL_SERVICE_USERNAME",
"value": "admin"
},
{
"name": "MYSQL_SERVICE_PASSWORD",
"value": "password"
},
{
"name": "MYSQL_SERVICE_DATABASE_NAME",
"value": "customers"
},
{
"name": "MYSQL_TARGET_USERNAME",
"value": "targetUser"
},
{
"name": "MYSQL_TARGET_PASSWORD",
"value": "targetPassword"
},
{
"name": "MYSQL_TARGET_DATABASE_NAME",
"value": "targetCustomers"
}
]
}
... and we could use that by adding to our project an injection point with the qualifier @ServiceName("mysql-target").
You can find similar examples inside the Fabric8 quickstarts. And more specifically the camel-cdi-amq quick start.
Stay tuned
I hope you enjoyed it. There are going to be more related topics soon (including writing integration tests for Java application running on Kubernetes).
Published at DZone with permission of Ioannis Canellos, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments