/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jooby.consul;
import java.text.MessageFormat;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.jooby.Env;
import org.jooby.Jooby;
import com.google.inject.Binder;
import com.orbitz.consul.AgentClient;
import com.orbitz.consul.Consul;
import com.orbitz.consul.model.agent.ImmutableRegistration;
import com.orbitz.consul.model.agent.Registration;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
/**
* <p>
* Consul client module.
* </p>
*
* <p>
* Exports the {@link Consul} client.
* </p>
*
* <p>
* Also register the application as a service and setup a health check.
* </p>
*
* <h1>usage</h1>
*
* <pre>{@code
* {
* use(new Consulby());
*
* get("/myservice/health", req {@literal ->} {
* Consul consul = require(Consul.class);
* List<ServiceHealth> serviceHealths = consul.healthClient()
* .getHealthyServiceInstances("myservice")
* .getResponse();
* return serviceHealths;
* });
* }
* }</pre>
*
* <h1>configuration</h1>
*
* <p>
* Configuration is done via <code>.conf</code>.
* </p>
*
* <p>
* For example, one can change the consul endpoint url,
* change the advertised service host, and disable registration health check:
* </p>
*
* <pre>
* consul.default.url = "http://consul.internal.domain.com:8500"
* consul.default.register.host = 10.0.0.2
* consul.default.register.check = null
* </pre>
*
* <p>
* or, disable the automatic registration feature completely:
* </p>
*
* <pre>
* consul.default.register = null
* </pre>
*
* @since 1.1.1
*/
public class Consulby implements Jooby.Module {
private final String name;
private Consumer<Consul.Builder> consulBuilderConsumer;
private Consumer<ImmutableRegistration.Builder> registrationBuilderConsumer;
/**
* A new {@link Consulby} instance, with the default config name: <code>default</code>.
*/
public Consulby() {
this("default");
}
/**
* <p>
* A new {@link Consulby} instance, with a provided config name.
* </p>
*
* <p>
* The module can be instantiated more than one time to allow connecting to many Consul
* installations:
* </p>
*
* <pre>
* {
* use(new Consulby("consul1"));
* use(new Consulby("consul2"));
* }
* </pre>
*
* <p>
* Since the module will fallback on the <code>consul.default</code> config prefix,
* it is possible to only override the desired properties in the <code>.conf</code>,
* for example, here, disabling health check only for `consul2`:
* </p>
*
* <pre>
* consul.consul1.url = "http://consul1.internal.domain.com:8500"
*
* consul.consul2.url = "http://consul2.internal.domain.com:8500"
* consul.consul2.register.check = null
* </pre>
*
* @param name A config name
*/
public Consulby(final String name) {
this.name = Objects.requireNonNull(name, "A consul config name is required.");
}
/**
* <p>
* {@link Consul} object can be configured programmatically:
* </p>
*
* <pre>{@code
* {
* use(new Consulby()
* .withConsulBuilder(consulBuilder -> {
* consulBuilder.withPing(false);
* consulBuilder.withBasicAuth("admin", "changeme");
* }));
* }
* }</pre>
*
* @param consulBuilderConsumer A {@link Consumer} that accepts {@link Consul.Builder}
* @return This {@link Consulby} to allow chaining
*/
public Consulby withConsulBuilder(final Consumer<Consul.Builder> consulBuilderConsumer) {
this.consulBuilderConsumer = consulBuilderConsumer;
return this;
}
/**
* <p>
* {@link Registration} object can be configured programmatically:
* </p>
*
* <pre>{@code
* {
* use(new Consulby()
* .withRegistrationBuilder(registrationBuilder -> {
* registrationBuilder.enableTagOverride(true);
* registrationBuilder.id("custom-service-id");
* }));
* }
* }</pre>
*
* @param registrationBuilderConsumer A {@link Consumer} that accepts
* {@link ImmutableRegistration.Builder}
* @return This {@link Consulby} to allow chaining
*/
public Consulby withRegistrationBuilder(
final Consumer<ImmutableRegistration.Builder> registrationBuilderConsumer) {
this.registrationBuilderConsumer = registrationBuilderConsumer;
return this;
}
@Override
public void configure(final Env env, final Config config, final Binder binder) throws Throwable {
Config consulConfig = config.getConfig("consul.default");
if (!name.equals("default") && config.hasPath("consul." + name)) {
consulConfig = config.getConfig("consul." + name).withFallback(consulConfig);
}
Consul.Builder consulBuilder = Consul.builder()
.withUrl(consulConfig.getString("url"));
if (consulBuilderConsumer != null) {
consulBuilderConsumer.accept(consulBuilder);
}
Consul consul = consulBuilder.build();
env.onStop(consul::destroy);
env.serviceKey().generate(Consul.class, name, k -> binder.bind(k).toInstance(consul));
if (consulConfig.hasPath("register")) {
Config registerConfig = consulConfig.getConfig("register");
ImmutableRegistration.Builder registrationBuilder = ImmutableRegistration.builder()
.name(registerConfig.getString("name"))
.address(registerConfig.getString("host"))
.port(registerConfig.getInt("port"))
.tags(registerConfig.getStringList("tags"))
.id(UUID.randomUUID().toString());
if (registerConfig.hasPath("check")) {
Config checkConfig = registerConfig.getConfig("check");
String http = MessageFormat.format("http://{0}:{1,number,####}{2}",
registerConfig.getString("host"),
registerConfig.getInt("port"),
checkConfig.getString("path"));
Registration.RegCheck check = Registration.RegCheck.http(http,
checkConfig.getDuration("interval", TimeUnit.SECONDS),
checkConfig.getDuration("timeout", TimeUnit.SECONDS));
registrationBuilder.check(check);
String response = checkConfig.getString("response");
env.router().get(checkConfig.getString("path"), () -> response);
}
if (registrationBuilderConsumer != null) {
registrationBuilderConsumer.accept(registrationBuilder);
}
Registration registration = registrationBuilder.build();
AgentClient agentClient = consul.agentClient();
env.onStarted(() -> agentClient.register(registration));
env.onStop(() -> agentClient.deregister(registration.getId()));
}
}
@Override
public Config config() {
return ConfigFactory.parseResources(getClass(), "consul.conf");
}
}