io.vertx.servicediscovery.kubernetes.KubernetesServiceImporter Maven / Gradle / Ivy
/*
* Copyright (c) 2011-2016 The original author or authors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.vertx.servicediscovery.kubernetes;
import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.impl.ContextInternal;
import io.vertx.core.impl.logging.Logger;
import io.vertx.core.impl.logging.LoggerFactory;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.parsetools.JsonParser;
import io.vertx.servicediscovery.Record;
import io.vertx.servicediscovery.spi.ServiceImporter;
import io.vertx.servicediscovery.spi.ServicePublisher;
import io.vertx.servicediscovery.spi.ServiceType;
import io.vertx.servicediscovery.types.*;
import java.util.*;
import java.util.stream.Stream;
import static io.vertx.core.http.HttpMethod.GET;
import static java.lang.Boolean.parseBoolean;
import static java.util.stream.Collectors.toSet;
/**
* A discovery bridge listening for kubernetes services and publishing them in the Vert.x service discovery.
* This bridge only supports the importation of services from kubernetes in vert.x (and not the opposite).
*
* The bridge is configured using:
*
* * the oauth token (using the content of `/var/run/secrets/kubernetes.io/serviceaccount/token` by default)
* * the namespace in which the service are searched (defaults to `default`).
*
* Be aware that the application must have access to Kubernetes and must be able to read the chosen namespace.
*
* {@link Record} are created from Kubernetes Service. The service type is deduced from the `service-type` label. If
* not set, the service is imported as `unknown`. Only `http-endpoint` are supported for now.
*
* @author Clement Escoffier
*/
public class KubernetesServiceImporter implements ServiceImporter {
private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesServiceImporter.class.getName());
private static final Set SUPPORTED_EVENT_TYPES = Stream.of(
"BOOKMARK",
"ADDED",
"DELETED",
"ERROR",
"MODIFIED"
).collect(toSet());
public static final String KUBERNETES_UUID = "kubernetes.uuid";
private final Map records = new HashMap<>();
private ContextInternal context;
private ServicePublisher publisher;
private String token;
private String namespace;
private HttpClient client;
private String lastResourceVersion;
private BatchOfUpdates batchOfUpdates;
private volatile boolean stop;
private static final String OPENSHIFT_KUBERNETES_TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token";
@Override
public void start(Vertx vertx, ServicePublisher publisher, JsonObject configuration, Promise completion) {
context = (ContextInternal) vertx.getOrCreateContext();
context.runOnContext(v -> init(publisher, configuration, completion));
}
private void init(ServicePublisher publisher, JsonObject configuration, Promise completion) {
this.publisher = publisher;
JsonObject conf;
if (configuration == null) {
conf = new JsonObject();
} else {
conf = configuration;
}
int port = conf.getInteger("port", 0);
if (port == 0) {
if (conf.getBoolean("ssl", true)) {
port = 443;
} else {
port = 80;
}
}
String p = System.getenv("KUBERNETES_SERVICE_PORT");
if (p != null) {
port = Integer.parseInt(p);
}
String host = conf.getString("host");
String h = System.getenv("KUBERNETES_SERVICE_HOST");
if (h != null) {
host = h;
}
client = context.owner().createHttpClient(new HttpClientOptions()
.setTrustAll(true)
.setSsl(conf.getBoolean("ssl", true))
.setDefaultHost(host)
.setDefaultPort(port)
);
// Retrieve token
Future retrieveTokenFuture = retrieveToken(conf);
// 1) get kubernetes auth info
this.namespace = conf.getString("namespace", getNamespaceOrDefault());
LOGGER.info("Kubernetes discovery configured for namespace: " + namespace);
LOGGER.info("Kubernetes master url: http" + (conf.getBoolean("ssl", true) ? "s" : "") + "//" + host + ":" + port);
retrieveTokenFuture
.compose(v -> retrieveServices())
.onSuccess(items -> LOGGER.info("Kubernetes initial import of " + items.size() + " services"))
.compose(this::publishRecords)
.onComplete(ar -> {
if (ar.succeeded()) {
LOGGER.info("Kubernetes importer instantiated with " + records.size() + " services imported");
completion.complete();
watch();
} else {
LOGGER.error("Error while interacting with kubernetes", ar.cause());
completion.fail(ar.cause());
}
});
}
private Future retrieveServices() {
String path = "/api/v1/namespaces/" + namespace + "/services";
return client.request(GET, path).compose(request -> {
request.setFollowRedirects(true);
request.putHeader("Authorization", "Bearer " + token);
return request.send();
}).compose(response -> {
return response.body().compose(body -> {
if (response.statusCode() != 200) {
return context.failedFuture("Unable to retrieve services from namespace " + namespace + ", status code: "
+ response.statusCode() + ", content: " + body.toString());
} else {
JsonObject serviceList = body.toJsonObject();
lastResourceVersion = serviceList.getJsonObject("metadata").getString("resourceVersion");
JsonArray items = serviceList.getJsonArray("items");
if (!serviceList.containsKey("items")) {
return context.failedFuture("Unable to retrieve services from namespace " + namespace + " - no items");
} else {
return context.succeededFuture(items);
}
}
});
});
}
private CompositeFuture publishRecords(JsonArray items) {
List publications = new ArrayList<>();
items.forEach(s -> {
JsonObject svc = ((JsonObject) s);
Record record = createRecord(svc);
if (addRecordIfNotContained(record)) {
Promise promise = context.promise();
publishRecord(record, promise);
publications.add(promise.future());
}
});
return CompositeFuture.all(publications);
}
private void watch() {
if (stop) {
return;
}
String path = "/api/v1/namespaces/" + namespace + "/services?"
+ "watch=true"
+ "&"
+ "allowWatchBookmarks=true"
+ "&"
+ "resourceVersion=" + lastResourceVersion;
JsonParser parser = JsonParser.newParser().objectValueMode()
.handler(event -> addToBatch(event.objectValue()));
client.request(GET, path).compose(request -> {
request.setFollowRedirects(true);
request.putHeader("Authorization", "Bearer " + token);
return request.send();
}).compose(response -> {
Promise promise = Promise.promise();
if (response.statusCode() == 200) {
LOGGER.info("Watching services from namespace " + namespace);
response
.exceptionHandler(t -> promise.tryComplete())
.endHandler(v -> promise.tryComplete())
.handler(parser);
} else {
promise.fail("");
}
return promise.future();
}).onComplete(res -> {
if (res.succeeded()) {
watch();
} else {
LOGGER.error("Failure while watching service list", res.cause());
fetchAndWatch();
}
});
}
private void fetchAndWatch() {
if (!stop) {
context.setTimer(2000, l -> {
retrieveServices()
.compose(this::publishRecords)
.onComplete(res -> {
if (res.succeeded()) {
watch();
} else {
fetchAndWatch();
}
});
});
}
}
private void addToBatch(JsonObject json) {
if (batchOfUpdates == null) {
long timerId = context.setTimer(500, l -> processBatch());
batchOfUpdates = new BatchOfUpdates(context.owner(), timerId);
}
batchOfUpdates.objects.add(json);
}
private void processBatch() {
Map