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]

Reply via email to