Skip to content

Commit

Permalink
[BOUNTY-4] Add NAT Kubernetes Support (#410)
Browse files Browse the repository at this point in the history
* add kubernetes support

Signed-off-by: Karim TAAM <[email protected]>

* fix review issues

Signed-off-by: Karim TAAM <[email protected]>
  • Loading branch information
matkt authored Feb 19, 2020
1 parent 1b0dffc commit 8a68402
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 2 deletions.
11 changes: 10 additions & 1 deletion besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
import org.hyperledger.besu.nat.core.NatManager;
import org.hyperledger.besu.nat.docker.DockerDetector;
import org.hyperledger.besu.nat.docker.DockerNatManager;
import org.hyperledger.besu.nat.kubernetes.KubernetesDetector;
import org.hyperledger.besu.nat.kubernetes.KubernetesNatManager;
import org.hyperledger.besu.nat.manual.ManualNatManager;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.plugin.BesuPlugin;
Expand All @@ -114,10 +116,14 @@
import com.google.common.base.Preconditions;
import graphql.GraphQL;
import io.vertx.core.Vertx;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.tuweni.bytes.Bytes;

public class RunnerBuilder {

protected static final Logger LOG = LogManager.getLogger();

private Vertx vertx;
private BesuController<?> besuController;

Expand Down Expand Up @@ -346,6 +352,7 @@ public Runner build() {
.map(nodePerms -> PeerPermissions.combine(nodePerms, bannedNodes))
.orElse(bannedNodes);

LOG.info("Detecting NAT service.");
final NatService natService = new NatService(buildNatManager(natMethod));
final NetworkBuilder inactiveNetwork = (caps) -> new NoopP2PNetwork();
final NetworkBuilder activeNetwork =
Expand Down Expand Up @@ -590,7 +597,7 @@ private Optional<NatManager> buildNatManager(final NatMethod natMethod) {
final NatMethod detectedNatMethod =
Optional.of(natMethod)
.filter(not(isEqual(NatMethod.AUTO)))
.orElse(NatService.autoDetectNatMethod(new DockerDetector()));
.orElse(NatService.autoDetectNatMethod(new DockerDetector(), new KubernetesDetector()));
switch (detectedNatMethod) {
case UPNP:
return Optional.of(new UpnpNatManager());
Expand All @@ -600,6 +607,8 @@ private Optional<NatManager> buildNatManager(final NatMethod natMethod) {
case DOCKER:
return Optional.of(
new DockerNatManager(p2pAdvertisedHost, p2pListenPort, jsonRpcConfiguration.getPort()));
case KUBERNETES:
return Optional.of(new KubernetesNatManager());
case NONE:
default:
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,9 @@ public void natMethodOptionIsParsedCorrectly() {
parseCommand("--nat-method", "DOCKER");
verify(mockRunnerBuilder).natMethod(eq(NatMethod.DOCKER));

parseCommand("--nat-method", "KUBERNETES");
verify(mockRunnerBuilder).natMethod(eq(NatMethod.KUBERNETES));

assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
Expand All @@ -1407,7 +1410,7 @@ public void parsesInvalidNatMethodOptionsShouldFail() {
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains(
"Invalid value for option '--nat-method': expected one of [UPNP, MANUAL, DOCKER, AUTO, NONE] (case-insensitive) but was 'invalid'");
"Invalid value for option '--nat-method': expected one of [UPNP, MANUAL, DOCKER, KUBERNETES, AUTO, NONE] (case-insensitive) but was 'invalid'");
}

@Test
Expand Down
2 changes: 2 additions & 0 deletions gradle/versions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ dependencyManagement {

dependency 'org.xerial.snappy:snappy-java:1.1.7.3'

dependency 'io.kubernetes:client-java:5.0.0'

dependency 'tech.pegasys.ethsigner.internal:core:0.4.0'
dependency 'tech.pegasys.ethsigner.internal:file-based:0.4.0'
dependency 'tech.pegasys.ethsigner.internal:signing-api:0.4.0'
Expand Down
1 change: 1 addition & 0 deletions nat/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
implementation 'org.apache.logging.log4j:log4j-api'
implementation 'org.jupnp:org.jupnp'
implementation 'org.jupnp:org.jupnp.support'
implementation 'io.kubernetes:client-java'

runtimeOnly 'org.apache.logging.log4j:log4j-core'

Expand Down
1 change: 1 addition & 0 deletions nat/src/main/java/org/hyperledger/besu/nat/NatMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public enum NatMethod {
UPNP,
MANUAL,
DOCKER,
KUBERNETES,
AUTO,
NONE;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright ConsenSys AG.
*
* Licensed 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/

package org.hyperledger.besu.nat.kubernetes;

import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.core.NatMethodDetector;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;

public class KubernetesDetector implements NatMethodDetector {

// When a Pod runs on a Node, the kubelet adds a set of environment variables for each active
// Service.
// https://kubernetes.io/docs/concepts/services-networking/connect-applications-service/#environment-variables
private static final Optional<String> KUBERNETES_SERVICE_HOST =
Optional.ofNullable(System.getenv("KUBERNETES_SERVICE_HOST"));
private static final Path KUBERNETES_WATERMARK_FILE = Paths.get("var/run/secrets/kubernetes.io");

@Override
public Optional<NatMethod> detect() {
return KUBERNETES_SERVICE_HOST
.map(__ -> NatMethod.KUBERNETES)
.or(
() ->
Files.exists(KUBERNETES_WATERMARK_FILE)
? Optional.of(NatMethod.KUBERNETES)
: Optional.empty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright ConsenSys AG.
*
* Licensed 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/

package org.hyperledger.besu.nat.kubernetes;

import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.core.AbstractNatManager;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;

import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

import com.google.common.annotations.VisibleForTesting;
import io.kubernetes.client.ApiClient;
import io.kubernetes.client.Configuration;
import io.kubernetes.client.apis.CoreV1Api;
import io.kubernetes.client.models.V1Service;
import io.kubernetes.client.util.ClientBuilder;
import io.kubernetes.client.util.KubeConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
* This class describes the behaviour of the Kubernetes NAT manager. Kubernetes Nat manager add
* support for Kubernetes’s NAT implementation when Besu is being run from a Kubernetes cluster
*/
public class KubernetesNatManager extends AbstractNatManager {
protected static final Logger LOG = LogManager.getLogger();

private static final String KUBE_CONFIG_PATH_ENV = "KUBE_CONFIG_PATH";
private static final String DEFAULT_KUBE_CONFIG_PATH = "~/.kube/config";
private static final String DEFAULT_BESU_POD_NAME_FILTER = "besu";

private String internalAdvertisedHost;
private final List<NatPortMapping> forwardedPorts = new ArrayList<>();

public KubernetesNatManager() {
super(NatMethod.KUBERNETES);
}

@Override
protected void doStart() {
LOG.info("Starting kubernetes NAT manager.");
update();
}

private void update() {
try {
LOG.debug("Trying to update information using Kubernetes client SDK.");
final String kubeConfigPath =
Optional.ofNullable(System.getenv(KUBE_CONFIG_PATH_ENV)).orElse(DEFAULT_KUBE_CONFIG_PATH);
LOG.debug(
"Checking if Kubernetes config file is present on file system: {}.", kubeConfigPath);
if (!Files.exists(Paths.get(kubeConfigPath))) {
throw new IllegalStateException("Cannot locate Kubernetes config file.");
}
// loading the out-of-cluster config, a kubeconfig from file-system
final ApiClient client =
ClientBuilder.kubeconfig(
KubeConfig.loadKubeConfig(
Files.newBufferedReader(Paths.get(kubeConfigPath), Charset.defaultCharset())))
.build();

// set the global default api-client to the in-cluster one from above
Configuration.setDefaultApiClient(client);

// the CoreV1Api loads default api-client from global configuration.
CoreV1Api api = new CoreV1Api();
// invokes the CoreV1Api client
api.listServiceForAllNamespaces(null, null, null, null, null, null, null, null, null)
.getItems().stream()
.filter(
v1Service -> v1Service.getMetadata().getName().contains(DEFAULT_BESU_POD_NAME_FILTER))
.findFirst()
.ifPresent(this::updateUsingBesuService);

} catch (Exception e) {
LOG.warn("Failed update information using Kubernetes client SDK.", e);
}
}

@VisibleForTesting
void updateUsingBesuService(final V1Service service) {
try {
LOG.info("Found Besu service: {}", service.getMetadata().getName());
LOG.info("Setting host IP to: {}.", service.getSpec().getClusterIP());
internalAdvertisedHost = service.getSpec().getClusterIP();
final String internalHost = queryLocalIPAddress().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
service
.getSpec()
.getPorts()
.forEach(
v1ServicePort -> {
try {
final NatServiceType natServiceType =
NatServiceType.fromString(v1ServicePort.getName());
forwardedPorts.add(
new NatPortMapping(
natServiceType,
natServiceType.equals(NatServiceType.DISCOVERY)
? NetworkProtocol.UDP
: NetworkProtocol.TCP,
internalHost,
internalAdvertisedHost,
v1ServicePort.getPort(),
v1ServicePort.getTargetPort().getIntValue()));
} catch (IllegalStateException e) {
LOG.warn("Ignored unknown Besu port: {}", e.getMessage());
}
});
} catch (Exception e) {
LOG.warn("Failed update information using pod metadata.", e);
}
}

@Override
protected void doStop() {
LOG.info("Stopping kubernetes NAT manager.");
}

@Override
protected CompletableFuture<String> retrieveExternalIPAddress() {
return CompletableFuture.completedFuture(internalAdvertisedHost);
}

@Override
public CompletableFuture<List<NatPortMapping>> getPortMappings() {
return CompletableFuture.completedFuture(forwardedPorts);
}
}
Loading

0 comments on commit 8a68402

Please sign in to comment.