Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BOUNTY-4] Add NAT Kubernetes Support #410

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if it would return the NatMethod for both of these? Do we want Docker to be returned even if Kubernetes would be detected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first method detected would be the one selected. In the order they are declared. However in that case Kubernetes would be detected. The detection method for docker and kubernetes are mutually exclusive.

Copy link
Contributor

@RatanRSur RatanRSur Feb 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detection method for docker and kubernetes are mutually exclusive.

I understand that only one would end up being selected but is it possible a misconfigured docker container inside a kubernetes "pod" (or whatever the term is haha), could succeed on the docker detector and not go on to the kubernetes detector? I ask because my intuition tells me that we should attempt to detect if we're in kubernetes first since it's the case that is a less general case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detection method for docker and kubernetes are mutually exclusive.
I understand that only one would end up being selected but is it possible a misconfigured docker container inside a kubernetes "pod" (or whatever the term is haha), could succeed on the docker detector and not go on to the kubernetes detector? I ask because my intuition tells me that we should attempt to detect if we're in kubernetes first since it's the case that is a less general case.

Ok I can create a new PR in order to change the order and put kubernetes detector first.

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,47 @@
/*
* 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.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 String KUBERNETES_SERVICE_HOST = "KUBERNETES_SERVICE_HOST";
private static final String KUBERNETES_WATERMARK_FILE = "/var/run/secrets/kubernetes.io";
RatanRSur marked this conversation as resolved.
Show resolved Hide resolved

@Override
public Optional<NatMethod> detect() {
return detectKubernetesServiceHost()
.map(__ -> NatMethod.KUBERNETES)
.or(
() ->
Files.exists(Paths.get(KUBERNETES_WATERMARK_FILE))
? Optional.of(NatMethod.KUBERNETES)
: Optional.empty());
}

public static Optional<String> detectKubernetesServiceHost() {
return Optional.ofNullable(System.getenv(KUBERNETES_SERVICE_HOST));
}
}
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