This is an automated email from the ASF dual-hosted git repository.
kishoreg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 0f58627 Added configurable Opt-In http listeners for controller API
(#5543)
0f58627 is described below
commit 0f586271068c7ba4e6ccce052479518e2c090908
Author: Daniel Lavoie <[email protected]>
AuthorDate: Fri Jun 19 22:50:45 2020 -0400
Added configurable Opt-In http listeners for controller API (#5543)
Introduction of new properties:
controller.access.protocols=<comma seperated protocols eg: http,https>
controller.access.protocols.<protocol>.port=9443
controller.access.protocols.<protocol>.host=0.0.0.0
controller.access.protocols.<protocol>.vip=false
controller.access.protocols.https.tls.keystore.path=some-path
controller.access.protocols.https.tls.keystore.password=pass
controller.access.protocols.https.tls.requires_client_auth=false
controller.access.protocols.https.tls.truststore.path=some-path
controller.access.protocols.https.tls.truststore.password=pass
Only the 'port' property will be mandatory if a protocol is activated
through controller.access.protcols
The existing 'controller.port property' is still expected by CLI. These
new properties are opt-in and can be activated with legacy
'controller.port' for backward compatibility.
---
.../apache/pinot/controller/ControllerConf.java | 43 +++-
.../apache/pinot/controller/ControllerStarter.java | 40 ++--
.../api/ControllerAdminApiApplication.java | 129 ++++++++----
.../controller/api/listeners/ListenerConfig.java | 59 ++++++
.../controller/api/listeners/TlsConfiguration.java | 63 ++++++
.../pinot/controller/util/ListenerConfigUtil.java | 110 ++++++++++
.../controller/util/ListenerConfigUtilTest.java | 226 +++++++++++++++++++++
.../apache/pinot/tools/utils/PinotConfigUtils.java | 58 +++++-
8 files changed, 662 insertions(+), 66 deletions(-)
diff --git
a/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerConf.java
b/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerConf.java
index f4ad1db..b3037ca 100644
---
a/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerConf.java
+++
b/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerConf.java
@@ -20,9 +20,13 @@ package org.apache.pinot.controller;
import java.io.File;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
+import java.util.Optional;
import java.util.Random;
+import java.util.stream.Collectors;
+
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
@@ -30,21 +34,18 @@ import
org.apache.helix.controller.rebalancer.strategy.AutoRebalanceStrategy;
import org.apache.pinot.common.protocols.SegmentCompletionProtocol;
import org.apache.pinot.common.utils.StringUtil;
import org.apache.pinot.spi.filesystem.LocalPinotFS;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import static
org.apache.pinot.common.utils.CommonConstants.Controller.CONFIG_OF_CONTROLLER_METRICS_PREFIX;
import static
org.apache.pinot.common.utils.CommonConstants.Controller.DEFAULT_METRICS_PREFIX;
public class ControllerConf extends PropertiesConfiguration {
- private static final Logger LOGGER =
LoggerFactory.getLogger(ControllerConf.class);
-
private static final String CONTROLLER_VIP_HOST = "controller.vip.host";
private static final String CONTROLLER_VIP_PORT = "controller.vip.port";
private static final String CONTROLLER_VIP_PROTOCOL =
"controller.vip.protocol";
private static final String CONTROLLER_HOST = "controller.host";
private static final String CONTROLLER_PORT = "controller.port";
+ private static final String CONTROLLER_ACCESS_PROTOCOLS =
"controller.access.protocols";
private static final String DATA_DIR = "controller.data.dir";
// Potentially same as data dir if local
private static final String LOCAL_TEMP_DIR = "controller.local.temp.dir";
@@ -283,6 +284,23 @@ public class ControllerConf extends
PropertiesConfiguration {
return (String) getProperty(CONTROLLER_PORT);
}
+ public List<String> getControllerAccessProtocols() {
+ return Optional.ofNullable(getStringArray(CONTROLLER_ACCESS_PROTOCOLS))
+
+ .map(protocols ->
Arrays.stream(protocols).collect(Collectors.toList()))
+
+ // http will be defaulted only if the legacy controller.port property
is not defined.
+ .orElseGet(() -> getControllerPort() == null ? Arrays.asList("http") :
Arrays.asList());
+ }
+
+ public String getControllerAccessProtocolProperty(String protocol, String
property) {
+ return getString(CONTROLLER_ACCESS_PROTOCOLS + "." + protocol + "." +
property);
+ }
+
+ public String getControllerAccessProtocolProperty(String protocol, String
property, String defaultValue) {
+ return getString(CONTROLLER_ACCESS_PROTOCOLS + "." + protocol + "." +
property, defaultValue);
+ }
+
public String getDataDir() {
return (String) getProperty(DATA_DIR);
}
@@ -346,7 +364,22 @@ public class ControllerConf extends
PropertiesConfiguration {
if (containsKey(CONTROLLER_VIP_PORT) && ((String)
getProperty(CONTROLLER_VIP_PORT)).length() > 0) {
return (String) getProperty(CONTROLLER_VIP_PORT);
}
- return getControllerPort();
+
+ // Vip port is not explicitly defined. Initiate discovery from the
configured protocols.
+ return getControllerAccessProtocols().stream()
+
+ .filter(protocol ->
Boolean.parseBoolean(getControllerAccessProtocolProperty(protocol, "vip",
"false")))
+
+ .map(protocol ->
Optional.ofNullable(getControllerAccessProtocolProperty(protocol, "port")))
+
+ .filter(Optional::isPresent)
+
+ .map(Optional::get)
+
+ .findFirst()
+
+ // No protocol defines a port as VIP. Fallback on legacy
controller.port property.
+ .orElseGet(this::getControllerPort);
}
public String getControllerVipProtocol() {
diff --git
a/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerStarter.java
b/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerStarter.java
index 1eab79a..ce09bd1 100644
---
a/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerStarter.java
+++
b/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerStarter.java
@@ -18,20 +18,18 @@
*/
package org.apache.pinot.controller;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.primitives.Longs;
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import com.yammer.metrics.core.MetricsRegistry;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+
import org.apache.commons.configuration.Configuration;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
@@ -57,6 +55,7 @@ import
org.apache.pinot.common.utils.helix.LeadControllerUtils;
import org.apache.pinot.controller.api.ControllerAdminApiApplication;
import org.apache.pinot.controller.api.access.AccessControlFactory;
import org.apache.pinot.controller.api.events.MetadataEventNotifierFactory;
+import org.apache.pinot.controller.api.listeners.ListenerConfig;
import org.apache.pinot.controller.api.resources.ControllerFilePathProvider;
import
org.apache.pinot.controller.api.resources.InvalidControllerConfigException;
import org.apache.pinot.controller.helix.SegmentStatusChecker;
@@ -71,6 +70,7 @@ import
org.apache.pinot.controller.helix.core.retention.RetentionManager;
import
org.apache.pinot.controller.helix.core.statemodel.LeadControllerResourceMasterSlaveStateModelFactory;
import org.apache.pinot.controller.helix.core.util.HelixSetupUtils;
import org.apache.pinot.controller.helix.starter.HelixConfig;
+import org.apache.pinot.controller.util.ListenerConfigUtil;
import org.apache.pinot.controller.validation.BrokerResourceValidationManager;
import org.apache.pinot.controller.validation.OfflineSegmentIntervalChecker;
import org.apache.pinot.controller.validation.RealtimeSegmentValidationManager;
@@ -84,6 +84,11 @@ import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.primitives.Longs;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.yammer.metrics.core.MetricsRegistry;
+
public class ControllerStarter implements ServiceStartable {
private static final Logger LOGGER =
LoggerFactory.getLogger(ControllerStarter.class);
@@ -94,6 +99,7 @@ public class ControllerStarter implements ServiceStartable {
private static final String METADATA_EVENT_NOTIFIER_PREFIX =
"metadata.event.notifier";
private final ControllerConf _config;
+ private final List<ListenerConfig> _listenerConfigs;
private final ControllerAdminApiApplication _adminApp;
// TODO: rename this variable once it's full separated with Helix controller.
private final PinotHelixResourceManager _helixResourceManager;
@@ -137,8 +143,11 @@ public class ControllerStarter implements ServiceStartable
{
// Helix related settings.
_helixZkURL = HelixConfig.getAbsoluteZkPathForHelix(_config.getZkStr());
_helixClusterName = _config.getHelixClusterName();
+ _listenerConfigs = ListenerConfigUtil.buildListenerConfigs(_config);
+
String host = conf.getControllerHost();
- int port = Integer.parseInt(conf.getControllerPort());
+ int port = inferPort();
+
_helixControllerInstanceId = host + "_" + port;
_helixParticipantInstanceId =
LeadControllerUtils.generateParticipantInstanceId(host, port);
_isUpdateStateModel = _config.isUpdateSegmentStateModel();
@@ -155,8 +164,7 @@ public class ControllerStarter implements ServiceStartable {
// Initialize FunctionRegistry before starting the admin application
(PinotQueryResource requires it to compile
// queries)
FunctionRegistry.init();
- _adminApp =
- new
ControllerAdminApiApplication(_config.getQueryConsoleWebappPath(),
_config.getQueryConsoleUseHttps());
+ _adminApp = new ControllerAdminApiApplication();
// Do not use this before the invocation of {@link
PinotHelixResourceManager::start()}, which happens in {@link
ControllerStarter::start()}
_helixResourceManager = new PinotHelixResourceManager(_config);
_executorService =
@@ -178,6 +186,13 @@ public class ControllerStarter implements ServiceStartable
{
}
}
+ private int inferPort() {
+ return
Optional.ofNullable(_config.getControllerPort()).map(Integer::parseInt)
+
+ // Fall back to protocol listeners if legacy controller.port is
undefined.
+ .orElseGet(() ->
_listenerConfigs.stream().findFirst().map(ListenerConfig::getPort).get());
+ }
+
private void setupHelixSystemProperties() {
// NOTE: Helix will disconnect the manager and disable the instance if it
detects flapping (too frequent disconnect
// from ZooKeeper). Setting flapping time window to a small value can
avoid this from happening. Helix ignores the
@@ -354,8 +369,6 @@ public class ControllerStarter implements ServiceStartable {
final MetadataEventNotifierFactory metadataEventNotifierFactory =
MetadataEventNotifierFactory.loadFactory(_config.subset(METADATA_EVENT_NOTIFIER_PREFIX));
- int jerseyPort = Integer.parseInt(_config.getControllerPort());
-
LOGGER.info("Controller download url base: {}", _config.generateVipUrl());
LOGGER.info("Injecting configuration and resource managers to the API
context");
final MultiThreadedHttpConnectionManager connectionManager = new
MultiThreadedHttpConnectionManager();
@@ -378,11 +391,10 @@ public class ControllerStarter implements
ServiceStartable {
}
});
- _adminApp.start(jerseyPort);
- LOGGER.info("Started Jersey API on port {}", jerseyPort);
- LOGGER.info("Pinot controller ready and listening on port {} for API
requests", _config.getControllerPort());
- LOGGER.info("Controller services available at http://{}:{}/",
_config.getControllerHost(),
- _config.getControllerPort());
+ _adminApp.start(_listenerConfigs,
ListenerConfigUtil.shouldAdvertiseAsHttps(_listenerConfigs, _config));
+
+ _listenerConfigs.stream().forEach(listenerConfig ->
LOGGER.info("Controller services available at {}://{}:{}/",
+ listenerConfig.getProtocol(), listenerConfig.getHost(),
listenerConfig.getPort()));
_controllerMetrics.addCallbackGauge("dataDir.exists", () -> new
File(_config.getDataDir()).exists() ? 1L : 0L);
_controllerMetrics.addCallbackGauge("dataDir.fileOpLatencyMs", () -> {
diff --git
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
index 7ad31f2..c5f4915 100644
---
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
+++
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
@@ -18,43 +18,48 @@
*/
package org.apache.pinot.controller.api;
-import com.google.common.base.Preconditions;
-import io.swagger.jaxrs.config.BeanConfig;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
+import java.util.List;
+import java.util.stream.Collectors;
+
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
+
+import org.apache.pinot.controller.api.listeners.ListenerConfig;
+import org.apache.pinot.controller.api.listeners.TlsConfiguration;
import org.glassfish.grizzly.http.server.CLStaticHttpHandler;
import org.glassfish.grizzly.http.server.HttpServer;
+import org.glassfish.grizzly.http.server.NetworkListener;
+import org.glassfish.grizzly.ssl.SSLContextConfigurator;
+import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
+import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler;
import org.glassfish.jersey.server.ResourceConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.Preconditions;
+
+import io.swagger.jaxrs.config.BeanConfig;
+
public class ControllerAdminApiApplication extends ResourceConfig {
private static final Logger LOGGER =
LoggerFactory.getLogger(ControllerAdminApiApplication.class);
- private HttpServer httpServer;
- private URI baseUri;
- private boolean started = false;
+ private HttpServer _httpServer;
private static final String RESOURCE_PACKAGE =
"org.apache.pinot.controller.api.resources";
- private static String CONSOLE_WEB_PATH;
- private final boolean _useHttps;
-
- public ControllerAdminApiApplication(String consoleWebPath, boolean
useHttps) {
+
+ public ControllerAdminApiApplication() {
super();
- CONSOLE_WEB_PATH = consoleWebPath;
- _useHttps = useHttps;
- if (!CONSOLE_WEB_PATH.endsWith("/")) {
- CONSOLE_WEB_PATH += "/";
- }
+
packages(RESOURCE_PACKAGE);
// TODO See ControllerResponseFilter
// register(new LoggingFeature());
@@ -62,29 +67,61 @@ public class ControllerAdminApiApplication extends
ResourceConfig {
register(MultiPartFeature.class);
registerClasses(io.swagger.jaxrs.listing.ApiListingResource.class);
registerClasses(io.swagger.jaxrs.listing.SwaggerSerializers.class);
- register(new ContainerResponseFilter() {
- @Override
- public void filter(ContainerRequestContext containerRequestContext,
- ContainerResponseContext containerResponseContext)
- throws IOException {
-
containerResponseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
- }
- });
+ register(new CorsFilter());
// property("jersey.config.server.tracing.type", "ALL");
// property("jersey.config.server.tracing.threshold", "VERBOSE");
}
+ private SSLEngineConfigurator buildSSLEngineConfigurator(TlsConfiguration
tlsConfiguration) {
+ SSLContextConfigurator sslContextConfigurator = new
SSLContextConfigurator();
+
+ sslContextConfigurator.setKeyStoreFile(tlsConfiguration.getKeyStorePath());
+
sslContextConfigurator.setKeyStorePass(tlsConfiguration.getKeyStorePassword());
+
sslContextConfigurator.setTrustStoreFile(tlsConfiguration.getTrustStorePath());
+
sslContextConfigurator.setTrustStorePass(tlsConfiguration.getTrustStorePassword());
+
+ return new
SSLEngineConfigurator(sslContextConfigurator).setClientMode(false)
+
.setWantClientAuth(tlsConfiguration.isRequiresClientAuth()).setEnabledProtocols(new
String[] { "TLSv1.2 " });
+ }
+
public void registerBinder(AbstractBinder binder) {
register(binder);
}
+
+ private void configureListener(ListenerConfig listenerConfig, HttpServer
httpServer) {
+ final NetworkListener listener = new
NetworkListener(listenerConfig.getName() + "-" + listenerConfig.getPort(),
+ listenerConfig.getHost(), listenerConfig.getPort());
+
+ listener.getTransport().getWorkerThreadPoolConfig()
+ .setThreadFactory(new
ThreadFactoryBuilder().setNameFormat("grizzly-http-server-%d")
+ .setUncaughtExceptionHandler(new
JerseyProcessingUncaughtExceptionHandler()).build());
+
+ listener.setSecure(listenerConfig.getTlsConfiguration() != null);
+ if (listener.isSecure()) {
+
listener.setSSLEngineConfig(buildSSLEngineConfigurator(listenerConfig.getTlsConfiguration()));
+ }
+ httpServer.addListener(listener);
+ }
- public boolean start(int httpPort) {
+ public void start(List<ListenerConfig> listenerConfigs, boolean
advertiseHttps) {
// ideally greater than reserved port but then port 80 is also valid
- Preconditions.checkArgument(httpPort > 0);
- baseUri = URI.create("http://0.0.0.0:" + Integer.toString(httpPort) + "/");
- httpServer = GrizzlyHttpServerFactory.createHttpServer(baseUri, this);
-
- setupSwagger(httpServer);
+ Preconditions.checkNotNull(listenerConfigs);
+
+ // The URI is irrelevant since the default listener will be manually
rewritten.
+ _httpServer =
GrizzlyHttpServerFactory.createHttpServer(URI.create("http://0.0.0.0/"), this,
false);
+
+ // Listeners cannot be configured with the factory. Manual overrides is
required as instructed by Javadoc.
+ _httpServer.removeListener("grizzly");
+
+ listenerConfigs.forEach(listenerConfig->configureListener(listenerConfig,
_httpServer));
+
+ try {
+ _httpServer.start();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to start Http Server", e);
+ }
+
+ setupSwagger(_httpServer, advertiseHttps);
ClassLoader classLoader =
ControllerAdminApiApplication.class.getClassLoader();
@@ -94,34 +131,29 @@ public class ControllerAdminApiApplication extends
ResourceConfig {
// So, we setup specific handlers for static resource directory.
index.html is served directly
// by a jersey handler
- httpServer.getServerConfiguration()
+ _httpServer.getServerConfiguration()
.addHttpHandler(new CLStaticHttpHandler(classLoader,
"/static/query/"), "/query/");
- httpServer.getServerConfiguration().addHttpHandler(new
CLStaticHttpHandler(classLoader, "/static/css/"), "/css/");
- httpServer.getServerConfiguration().addHttpHandler(new
CLStaticHttpHandler(classLoader, "/static/js/"), "/js/");
+ _httpServer.getServerConfiguration().addHttpHandler(new
CLStaticHttpHandler(classLoader, "/static/css/"), "/css/");
+ _httpServer.getServerConfiguration().addHttpHandler(new
CLStaticHttpHandler(classLoader, "/static/js/"), "/js/");
// without this explicit request to /index.html will not work
- httpServer.getServerConfiguration().addHttpHandler(new
CLStaticHttpHandler(classLoader, "/static/"), "/index.html");
-
- started = true;
- LOGGER.info("Start jersey admin API on port: {}", httpPort);
- return true;
- }
+ _httpServer.getServerConfiguration().addHttpHandler(new
CLStaticHttpHandler(classLoader, "/static/"), "/index.html");
- public URI getBaseUri() {
- return baseUri;
+ LOGGER.info("Admin API started on ports: {}",
listenerConfigs.stream().map(ListenerConfig::getPort)
+ .map(port -> port.toString()).collect(Collectors.joining(",")));
}
- private void setupSwagger(HttpServer httpServer) {
+ private void setupSwagger(HttpServer httpServer, boolean advertiseHttps) {
BeanConfig beanConfig = new BeanConfig();
beanConfig.setTitle("Pinot Controller API");
beanConfig.setDescription("APIs for accessing Pinot Controller
information");
beanConfig.setContact("https://github.com/apache/incubator-pinot");
beanConfig.setVersion("1.0");
- if (_useHttps) {
+ if (advertiseHttps) {
beanConfig.setSchemes(new String[]{"https"});
} else {
beanConfig.setSchemes(new String[]{"http"});
}
- beanConfig.setBasePath(baseUri.getPath());
+ beanConfig.setBasePath("/");
beanConfig.setResourcePackage(RESOURCE_PACKAGE);
beanConfig.setScan(true);
@@ -137,9 +169,18 @@ public class ControllerAdminApiApplication extends
ResourceConfig {
}
public void stop() {
- if (!started) {
+ if (!_httpServer.isStarted()) {
return;
}
- httpServer.shutdownNow();
+ _httpServer.shutdownNow();
+ }
+
+ private class CorsFilter implements ContainerResponseFilter {
+ @Override
+ public void filter(ContainerRequestContext containerRequestContext,
+ ContainerResponseContext containerResponseContext)
+ throws IOException {
+ containerResponseContext.getHeaders().add("Access-Control-Allow-Origin",
"*");
+ }
}
}
diff --git
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/listeners/ListenerConfig.java
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/listeners/ListenerConfig.java
new file mode 100644
index 0000000..07df6a1
--- /dev/null
+++
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/listeners/ListenerConfig.java
@@ -0,0 +1,59 @@
+/**
+ * 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
+ *
+ * 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.
+ */
+package org.apache.pinot.controller.api.listeners;
+
+/**
+ * Provides configuration settings expected by an Http Server to
+ * setup listeners for http and https protocols.
+ */
+public class ListenerConfig {
+ private final String name;
+ private final String host;
+ private final int port;
+ private final String protocol;
+ private final TlsConfiguration tlsConfiguration;
+
+ public ListenerConfig(String name, String host, int port, String protocol,
TlsConfiguration tlsConfiguration) {
+ this.name = name;
+ this.host = host;
+ this.port = port;
+ this.protocol = protocol;
+ this.tlsConfiguration = tlsConfiguration;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public TlsConfiguration getTlsConfiguration() {
+ return tlsConfiguration;
+ }
+}
diff --git
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/listeners/TlsConfiguration.java
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/listeners/TlsConfiguration.java
new file mode 100644
index 0000000..d3edde0
--- /dev/null
+++
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/listeners/TlsConfiguration.java
@@ -0,0 +1,63 @@
+package org.apache.pinot.controller.api.listeners;
+
+/**
+ * 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
+ *
+ * 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.
+ */
+
+/**
+ * Holds TLS configuration settings. Used as a vessel to configure Https
Listeners.
+ *
+ * @author Daniel Lavoie
+ * @since 0.4.0
+ */
+public class TlsConfiguration {
+ private final String keyStorePath;
+ private final String keyStorePassword;
+ private final String trustStorePath;
+ private final String trustStorePassword;
+ private final boolean requiresClientAuth;
+
+ public TlsConfiguration(String keyStorePath, String keyStorePassword, String
trustStorePath,
+ String trustStorePassword, boolean requiresClientAuth) {
+ this.keyStorePath = keyStorePath;
+ this.keyStorePassword = keyStorePassword;
+ this.trustStorePath = trustStorePath;
+ this.trustStorePassword = trustStorePassword;
+ this.requiresClientAuth = requiresClientAuth;
+ }
+
+ public String getKeyStorePath() {
+ return keyStorePath;
+ }
+
+ public String getKeyStorePassword() {
+ return keyStorePassword;
+ }
+
+ public String getTrustStorePath() {
+ return trustStorePath;
+ }
+
+ public String getTrustStorePassword() {
+ return trustStorePassword;
+ }
+
+ public boolean isRequiresClientAuth() {
+ return requiresClientAuth;
+ }
+}
diff --git
a/pinot-controller/src/main/java/org/apache/pinot/controller/util/ListenerConfigUtil.java
b/pinot-controller/src/main/java/org/apache/pinot/controller/util/ListenerConfigUtil.java
new file mode 100644
index 0000000..3780971
--- /dev/null
+++
b/pinot-controller/src/main/java/org/apache/pinot/controller/util/ListenerConfigUtil.java
@@ -0,0 +1,110 @@
+/**
+ * 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
+ *
+ * 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.
+ */
+package org.apache.pinot.controller.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.apache.pinot.controller.ControllerConf;
+import org.apache.pinot.controller.api.listeners.ListenerConfig;
+import org.apache.pinot.controller.api.listeners.TlsConfiguration;
+
+
+/**
+ * Utility class that generates Http {@link ListenerConfig} instances
+ * based on the properties provided by {@link ControllerConf}.
+ */
+public abstract class ListenerConfigUtil {
+
+ /**
+ * Generates {@link ListenerConfig} instances based on the compination
+ * of propperties such as controller.port and controller.access.protocols.
+ *
+ * @param controllerConf property holders for controller configuration
+ * @return List of {@link ListenerConfig} for which http listeners
+ * should be created.
+ */
+ public static List<ListenerConfig> buildListenerConfigs(ControllerConf
controllerConf) {
+ List<ListenerConfig> listenerConfigs = new ArrayList<>();
+
+ if (controllerConf.getControllerPort() != null) {
+ listenerConfigs.add(
+ new ListenerConfig("http", "0.0.0.0",
Integer.valueOf(controllerConf.getControllerPort()), "http", null));
+ }
+
+
listenerConfigs.addAll(controllerConf.getControllerAccessProtocols().stream()
+
+ .map(protocol -> buildListenerConfig(protocol, controllerConf))
+
+ .collect(Collectors.toList()));
+
+ return listenerConfigs;
+ }
+
+ /**
+ * Will determine if the query console should be advertised as http or https.
+ *
+ * The query console can be advertised as secured with
+ * {@link ControllerConf#getQueryConsoleUseHttps()} for cases for only http
+ * listeners are defined by the query console is served by a secured reverse
+ * proxy.
+ *
+ * @param listenerConfigs generated listener configs from {@link
ListenerConfigUtil#buildListenerConfigs(ControllerConf)}
+ * @param controllerConf property holders for controller configuration
+ * @return whether the query console should be advertised as http or https.
+ */
+ public static boolean shouldAdvertiseAsHttps(List<ListenerConfig>
listenerConfigs, ControllerConf controllerConf) {
+ return controllerConf.getQueryConsoleUseHttps() ||
!listenerConfigs.stream().map(ListenerConfig::getProtocol)
+ .filter(protocol -> protocol.equals("http")).findAny().isPresent();
+ }
+
+ private static Optional<TlsConfiguration> buildTlsConfiguration(String
protocol, ControllerConf controllerConf) {
+ return
Optional.ofNullable(controllerConf.getControllerAccessProtocolProperty(protocol,
"tls.keystore.path"))
+
+ .map(keystore -> buildTlsConfiguration(protocol, keystore,
controllerConf));
+ }
+
+ private static TlsConfiguration buildTlsConfiguration(String protocol,
String keystore,
+ ControllerConf controllerConf) {
+ return new TlsConfiguration(keystore,
+ controllerConf.getControllerAccessProtocolProperty(protocol,
"tls.keystore.password"),
+ controllerConf.getControllerAccessProtocolProperty(protocol,
"tls.truststore.path"),
+ controllerConf.getControllerAccessProtocolProperty(protocol,
"tls.truststore.password"), Boolean.parseBoolean(
+ controllerConf.getControllerAccessProtocolProperty(protocol,
"tls.requires_client_auth", "false")));
+ }
+
+ private static ListenerConfig buildListenerConfig(String protocol,
ControllerConf controllerConf) {
+ return new ListenerConfig(protocol,
+ getHost(controllerConf.getControllerAccessProtocolProperty(protocol,
"host", "0.0.0.0")),
+ getPort(controllerConf.getControllerAccessProtocolProperty(protocol,
"port")), protocol,
+ buildTlsConfiguration(protocol, controllerConf).orElse(null));
+ }
+
+ private static String getHost(String configuredHost) {
+ return Optional.ofNullable(configuredHost).filter(host ->
!host.trim().isEmpty())
+ .orElseThrow(() -> new IllegalArgumentException(configuredHost + " is
not a valid host"));
+ }
+
+ private static int getPort(String configuredPort) {
+ return Optional.ofNullable(configuredPort).filter(host ->
!host.trim().isEmpty()).<Integer> map(Integer::valueOf)
+ .orElseThrow(() -> new IllegalArgumentException(configuredPort + " is
not a valid port"));
+ }
+}
diff --git
a/pinot-controller/src/test/java/org/apache/pinot/controller/util/ListenerConfigUtilTest.java
b/pinot-controller/src/test/java/org/apache/pinot/controller/util/ListenerConfigUtilTest.java
new file mode 100644
index 0000000..772f4d2
--- /dev/null
+++
b/pinot-controller/src/test/java/org/apache/pinot/controller/util/ListenerConfigUtilTest.java
@@ -0,0 +1,226 @@
+/**
+ * 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
+ *
+ * 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.
+ */
+package org.apache.pinot.controller.util;
+
+import java.util.List;
+
+import org.apache.pinot.controller.ControllerConf;
+import org.apache.pinot.controller.api.listeners.ListenerConfig;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+
+/**
+ * Asserts that {@link ListenerConfigUtil} will generated expected {@link
ListenerConfig} based on the properties provided in {@link ControllerConf}
+ */
+public class ListenerConfigUtilTest {
+ /**
+ * Asserts that the protocol listeners properties are Opt-In and not
initialized when nothing but controler.port is used.
+ */
+ @Test
+ public void assertControllerPortConfig() {
+ ControllerConf controllerConf = new ControllerConf();
+
+ controllerConf.setProperty("controller.port", "9000");
+ controllerConf.setProperty("controller.query.console.useHttps", "true");
+
+ List<ListenerConfig> listenerConfigs =
ListenerConfigUtil.buildListenerConfigs(controllerConf);
+
+ Assert.assertEquals(listenerConfigs.size(), 1);
+
+ assertLegacyListener(listenerConfigs.get(0));
+
+
Assert.assertTrue(ListenerConfigUtil.shouldAdvertiseAsHttps(listenerConfigs,
controllerConf));
+ }
+
+ /**
+ * Asserts that enabling https generates the existing legacy listener as
well as the another one configured with TLS settings.
+ */
+ @Test
+ public void assertLegacyAndHttps() {
+ ControllerConf controllerConf = new ControllerConf();
+
+ controllerConf.setProperty("controller.port", "9000");
+ controllerConf.setProperty("controller.access.protocols", "https");
+
+ configureHttpsProperties(controllerConf, "10.0.0.10", 9443);
+
+ List<ListenerConfig> listenerConfigs =
ListenerConfigUtil.buildListenerConfigs(controllerConf);
+
+ Assert.assertEquals(listenerConfigs.size(), 2);
+
+ ListenerConfig legacyListener = getListener("http", listenerConfigs);
+ ListenerConfig httpsListener = getListener("https", listenerConfigs);
+
+ assertLegacyListener(legacyListener);
+ assertHttpsListener(httpsListener, "10.0.0.10", 9443);
+
+
Assert.assertFalse(ListenerConfigUtil.shouldAdvertiseAsHttps(listenerConfigs,
controllerConf));
+ }
+
+ /**
+ * Asserts that controller.port can be opt-out and both http and https can
be configured with seperate ports.
+ */
+ @Test
+ public void assertHttpAndHttpsConfigs() {
+ ControllerConf controllerConf = new ControllerConf();
+
+ controllerConf.setProperty("controller.access.protocols", "http,https");
+
+ configureHttpsProperties(controllerConf, 9443);
+
+ controllerConf.setProperty("controller.access.protocols.http.port",
"9000");
+
+ List<ListenerConfig> listenerConfigs =
ListenerConfigUtil.buildListenerConfigs(controllerConf);
+
+ Assert.assertEquals(listenerConfigs.size(), 2);
+
+ ListenerConfig httpListener = getListener("http", listenerConfigs);
+ ListenerConfig httpsListener = getListener("https", listenerConfigs);
+
+ Assert.assertEquals(httpListener.getHost(), "0.0.0.0");
+
+ assertHttpListener(httpListener, "0.0.0.0", 9000);
+ assertHttpsListener(httpsListener, "0.0.0.0", 9443);
+ }
+
+ /**
+ * Asserts that a single listener configuration is generated with a secured
TLS port.
+ */
+ @Test
+ public void assertHttpsOnly() {
+ ControllerConf controllerConf = new ControllerConf();
+
+ controllerConf.setProperty("controller.access.protocols", "https");
+
+ configureHttpsProperties(controllerConf, 9443);
+
+ List<ListenerConfig> listenerConfigs =
ListenerConfigUtil.buildListenerConfigs(controllerConf);
+
+ Assert.assertEquals(listenerConfigs.size(), 1);
+
+ assertHttpsListener(listenerConfigs.get(0), "0.0.0.0", 9443);
+
+
Assert.assertTrue(ListenerConfigUtil.shouldAdvertiseAsHttps(listenerConfigs,
controllerConf));
+ }
+
+ /**
+ * Tests behavior when an invalid host is provided.
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void assertInvalidHost() {
+ ControllerConf controllerConf = new ControllerConf();
+
+ controllerConf.setProperty("controller.access.protocols", "https");
+
+ configureHttpsProperties(controllerConf, "", 9443);
+
+ ListenerConfigUtil.buildListenerConfigs(controllerConf);
+ }
+
+ /**
+ * Tests behavior when an invalid port is provided
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void assertInvalidPort() {
+ ControllerConf controllerConf = new ControllerConf();
+
+ controllerConf.setProperty("controller.access.protocols", "https");
+
+ configureHttpsProperties(controllerConf, "", 9443);
+ controllerConf.setProperty("controller.access.protocol.https.port",
"10.10");
+
+ ListenerConfigUtil.buildListenerConfigs(controllerConf);
+ }
+
+ /**
+ * Tests behavior when an empty http port is provided.
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void assertEmptyHttpPort() {
+ ControllerConf controllerConf = new ControllerConf();
+
+ controllerConf.setProperty("controller.access.protocols", "http");
+ controllerConf.setProperty("controller.access.protocols.http.port", "");
+
+ ListenerConfigUtil.buildListenerConfigs(controllerConf);
+ }
+
+ /**
+ * Tests behavior when an empty https port is provided.
+ */
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void assertEmptyHttpsPort() {
+ ControllerConf controllerConf = new ControllerConf();
+
+ controllerConf.setProperty("controller.access.protocols", "https");
+ controllerConf.setProperty("controller.access.protocols.https.port", "");
+
+ ListenerConfigUtil.buildListenerConfigs(controllerConf);
+ }
+
+ private void assertLegacyListener(ListenerConfig legacyListener) {
+ Assert.assertEquals(legacyListener.getName(), "http");
+ Assert.assertEquals(legacyListener.getHost(), "0.0.0.0");
+ Assert.assertEquals(legacyListener.getPort(), 9000);
+ Assert.assertEquals(legacyListener.getProtocol(), "http");
+ Assert.assertNull(legacyListener.getTlsConfiguration());
+ }
+
+ private void assertHttpListener(ListenerConfig httpsListener, String host,
int port) {
+ Assert.assertEquals(httpsListener.getName(), "http");
+ Assert.assertEquals(httpsListener.getHost(), host);
+ Assert.assertEquals(httpsListener.getPort(), port);
+ Assert.assertEquals(httpsListener.getProtocol(), "http");
+ Assert.assertNull(httpsListener.getTlsConfiguration());
+ }
+
+ private void assertHttpsListener(ListenerConfig httpsListener, String host,
int port) {
+ Assert.assertEquals(httpsListener.getName(), "https");
+ Assert.assertEquals(httpsListener.getHost(), host);
+ Assert.assertEquals(httpsListener.getPort(), port);
+ Assert.assertEquals(httpsListener.getProtocol(), "https");
+ Assert.assertNotNull(httpsListener.getTlsConfiguration());
+
Assert.assertEquals(httpsListener.getTlsConfiguration().getKeyStorePassword(),
"a-password");
+ Assert.assertEquals(httpsListener.getTlsConfiguration().getKeyStorePath(),
"/some-keystore-path");
+
Assert.assertEquals(httpsListener.getTlsConfiguration().isRequiresClientAuth(),
true);
+
Assert.assertEquals(httpsListener.getTlsConfiguration().getTrustStorePassword(),
"a-password");
+
Assert.assertEquals(httpsListener.getTlsConfiguration().getTrustStorePath(),
"/some-truststore-path");
+ }
+
+ private void configureHttpsProperties(ControllerConf controllerConf, String
host, int port) {
+ if (host != null) {
+ controllerConf.setProperty("controller.access.protocols.https.host",
host);
+ }
+ controllerConf.setProperty("controller.access.protocols.https.port",
String.valueOf(port));
+
controllerConf.setProperty("controller.access.protocols.https.tls.keystore.password",
"a-password");
+
controllerConf.setProperty("controller.access.protocols.https.tls.keystore.path",
"/some-keystore-path");
+
controllerConf.setProperty("controller.access.protocols.https.tls.requires_client_auth",
"true");
+
controllerConf.setProperty("controller.access.protocols.https.tls.truststore.password",
"a-password");
+
controllerConf.setProperty("controller.access.protocols.https.tls.truststore.path",
"/some-truststore-path");
+ }
+
+ private void configureHttpsProperties(ControllerConf controllerConf, int
port) {
+ configureHttpsProperties(controllerConf, null, port);
+ }
+
+ private ListenerConfig getListener(String name, List<ListenerConfig>
listenerConfigs) {
+ return listenerConfigs.stream().filter(listenerConfig ->
listenerConfig.getName().equals(name)).findFirst().get();
+ }
+}
diff --git
a/pinot-tools/src/main/java/org/apache/pinot/tools/utils/PinotConfigUtils.java
b/pinot-tools/src/main/java/org/apache/pinot/tools/utils/PinotConfigUtils.java
index c19e5f4..fd508f1 100644
---
a/pinot-tools/src/main/java/org/apache/pinot/tools/utils/PinotConfigUtils.java
+++
b/pinot-tools/src/main/java/org/apache/pinot/tools/utils/PinotConfigUtils.java
@@ -23,6 +23,9 @@ import java.io.IOException;
import java.net.ServerSocket;
import java.net.SocketException;
import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Optional;
+
import org.apache.commons.configuration.BaseConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
@@ -114,7 +117,10 @@ public class PinotConfigUtils {
throw new ConfigurationException(
String.format(CONTROLLER_CONFIG_VALIDATION_ERROR_MESSAGE_FORMAT,
"null conf object."));
}
- if (conf.getControllerPort() == null) {
+
+ List<String> protocols = validateControllerAccessProtocols(conf);
+
+ if (conf.getControllerPort() == null && protocols.isEmpty()) {
throw new
ConfigurationException(String.format(CONTROLLER_CONFIG_VALIDATION_ERROR_MESSAGE_FORMAT,
"missing controller port, please specify 'controller.port' property
in config file."));
}
@@ -129,8 +135,54 @@ public class PinotConfigUtils {
return true;
}
- public static PropertiesConfiguration readConfigFromFile(String
configFileName)
- throws ConfigurationException {
+ private static List<String> validateControllerAccessProtocols(ControllerConf
conf) throws ConfigurationException {
+ List<String> protocols = conf.getControllerAccessProtocols();
+
+ if(!protocols.isEmpty()) {
+ Optional<String> invalidProtocol =
+ protocols.stream().filter(protocol -> !protocol.equals("http") &&
!protocol.equals("https")).findFirst();
+
+ if (invalidProtocol.isPresent()) {
+ throw new
ConfigurationException(String.format(CONTROLLER_CONFIG_VALIDATION_ERROR_MESSAGE_FORMAT,
+ invalidProtocol.get() + " is not a valid protocol for the
'controller.access.protocols' property."));
+ }
+
+ Optional<ConfigurationException> invalidPort = protocols.stream()
+ .map(protocol -> validatePort(protocol,
conf.getControllerAccessProtocolProperty(protocol, "port")))
+
+ .filter(Optional::isPresent)
+
+ .map(Optional::get)
+
+ .findAny();
+
+ if (invalidPort.isPresent()) {
+ throw invalidPort.get();
+ }
+ }
+
+ return protocols;
+ }
+
+ private static Optional<ConfigurationException> validatePort(String
protocol, String port) {
+ if (port == null) {
+ return Optional.of(new ConfigurationException(
+ String.format(CONTROLLER_CONFIG_VALIDATION_ERROR_MESSAGE_FORMAT,
"missing controller " + protocol
+ + " port, please fix 'controller.access.protocols." + protocol +
".port' property in config file.")));
+ }
+
+ try {
+ Integer.parseInt(port);
+ } catch (NumberFormatException e) {
+ return Optional.of(new
ConfigurationException(String.format(CONTROLLER_CONFIG_VALIDATION_ERROR_MESSAGE_FORMAT,
+ port + " is not a valid port, please fix
'controller.access.protocols." + protocol
+ + ".port' property in config file.")));
+ }
+
+ return Optional.empty();
+ }
+
+ public static PropertiesConfiguration readConfigFromFile(String
configFileName) throws ConfigurationException {
if (configFileName == null) {
return null;
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]