All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.kafka.connect.runtime.rest.RestServerConfig Maven / Gradle / Ivy

The newest version!
/*
 * 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.kafka.connect.runtime.rest;

import org.apache.kafka.common.config.AbstractConfig;
import org.apache.kafka.common.config.ConfigDef;
import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.common.config.SslClientAuth;
import org.apache.kafka.common.config.internals.BrokerSecurityConfigs;
import org.apache.kafka.common.utils.Utils;
import org.apache.kafka.connect.runtime.WorkerConfig;

import org.eclipse.jetty.util.StringUtil;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.apache.kafka.common.config.ConfigDef.ValidString.in;

/**
 * Defines the configuration surface for a {@link RestServer} instance, with support for both
 * {@link #forInternal(Map) internal-only} and {@link #forPublic(Integer, Map) user-facing}
 * servers. An internal-only server will expose only the endpoints and listeners necessary for
 * intra-cluster communication; these include the task-write and zombie-fencing endpoints. A
 * user-facing server will expose these endpoints and, in addition, all endpoints that are part of
 * the public REST API for Kafka Connect; these include the connector creation, connector
 * status, configuration validation, and logging endpoints. In addition, a user-facing server will
 * instantiate any user-configured
 * {@link RestServerConfig#REST_EXTENSION_CLASSES_CONFIG REST extensions}.
 */
public abstract class RestServerConfig extends AbstractConfig {

    public static final String LISTENERS_CONFIG = "listeners";
    private static final String LISTENERS_DOC
            = "List of comma-separated URIs the REST API will listen on. The supported protocols are HTTP and HTTPS.\n" +
            " Specify hostname as 0.0.0.0 to bind to all interfaces.\n" +
            " Leave hostname empty to bind to default interface.\n" +
            " Examples of legal listener lists: HTTP://myhost:8083,HTTPS://myhost:8084";
    // Visible for testing
    static final List LISTENERS_DEFAULT = Collections.singletonList("http://:8083");

    public static final String REST_ADVERTISED_HOST_NAME_CONFIG = "rest.advertised.host.name";
    private static final String REST_ADVERTISED_HOST_NAME_DOC
            = "If this is set, this is the hostname that will be given out to other workers to connect to.";

    public static final String REST_ADVERTISED_PORT_CONFIG = "rest.advertised.port";
    private static final String REST_ADVERTISED_PORT_DOC
            = "If this is set, this is the port that will be given out to other workers to connect to.";

    public static final String REST_ADVERTISED_LISTENER_CONFIG = "rest.advertised.listener";
    private static final String REST_ADVERTISED_LISTENER_DOC
            = "Sets the advertised listener (HTTP or HTTPS) which will be given to other workers to use.";

    public static final String ACCESS_CONTROL_ALLOW_ORIGIN_CONFIG = "access.control.allow.origin";
    private static final String ACCESS_CONTROL_ALLOW_ORIGIN_DOC =
            "Value to set the Access-Control-Allow-Origin header to for REST API requests." +
                    "To enable cross origin access, set this to the domain of the application that should be permitted" +
                    " to access the API, or '*' to allow access from any domain. The default value only allows access" +
                    " from the domain of the REST API.";
    protected static final String ACCESS_CONTROL_ALLOW_ORIGIN_DEFAULT = "";

    public static final String ACCESS_CONTROL_ALLOW_METHODS_CONFIG = "access.control.allow.methods";
    private static final String ACCESS_CONTROL_ALLOW_METHODS_DOC =
            "Sets the methods supported for cross origin requests by setting the Access-Control-Allow-Methods header. "
                    + "The default value of the Access-Control-Allow-Methods header allows cross origin requests for GET, POST and HEAD.";
    private static final String ACCESS_CONTROL_ALLOW_METHODS_DEFAULT = "";

    public static final String ADMIN_LISTENERS_CONFIG = "admin.listeners";
    private static final String ADMIN_LISTENERS_DOC = "List of comma-separated URIs the Admin REST API will listen on." +
            " The supported protocols are HTTP and HTTPS." +
            " An empty or blank string will disable this feature." +
            " The default behavior is to use the regular listener (specified by the 'listeners' property).";
    public static final String ADMIN_LISTENERS_HTTPS_CONFIGS_PREFIX = "admin.listeners.https.";

    public static final String REST_EXTENSION_CLASSES_CONFIG = "rest.extension.classes";
    private static final String REST_EXTENSION_CLASSES_DOC =
            "Comma-separated names of ConnectRestExtension classes, loaded and called "
                    + "in the order specified. Implementing the interface  "
                    + "ConnectRestExtension allows you to inject into Connect's REST API user defined resources like filters. "
                    + "Typically used to add custom capability like logging, security, etc. ";

    // Visible for testing
    static final String RESPONSE_HTTP_HEADERS_CONFIG = "response.http.headers.config";
    // Visible for testing
    static final String RESPONSE_HTTP_HEADERS_DOC = "Rules for REST API HTTP response headers";
    // Visible for testing
    static final String RESPONSE_HTTP_HEADERS_DEFAULT = "";
    private static final Collection HEADER_ACTIONS = Collections.unmodifiableList(
            Arrays.asList("set", "add", "setDate", "addDate")
    );


    /**
     * @return the listeners to use for this server, or empty if no admin endpoints should be exposed,
     * or null if the admin endpoints should be exposed on the {@link #listeners() regular listeners} for
     * this server
     */
    public abstract List adminListeners();

    /**
     * @return a list of {@link #REST_EXTENSION_CLASSES_CONFIG REST extension} classes
     * to instantiate and use with the server
     */
    public abstract List restExtensions();

    /**
     * @return whether {@link WorkerConfig#TOPIC_TRACKING_ENABLE_CONFIG topic tracking}
     * is enabled on this worker
     */
    public abstract boolean topicTrackingEnabled();

    /**
     * @return whether {@link WorkerConfig#TOPIC_TRACKING_ALLOW_RESET_CONFIG topic tracking resets}
     * are enabled on this worker
     */
    public abstract boolean topicTrackingResetEnabled();

    /**
     * Add the properties related to a user-facing server to the given {@link ConfigDef}.
     * 

* This automatically adds the properties for intra-cluster communication; it is not necessary to * invoke both {@link #addInternalConfig(ConfigDef)} and this method on the same {@link ConfigDef}. * @param configDef the {@link ConfigDef} to add the properties to; may not be null */ public static void addPublicConfig(ConfigDef configDef) { addInternalConfig(configDef); configDef .define( REST_EXTENSION_CLASSES_CONFIG, ConfigDef.Type.LIST, "", ConfigDef.Importance.LOW, REST_EXTENSION_CLASSES_DOC ).define(ADMIN_LISTENERS_CONFIG, ConfigDef.Type.LIST, null, new AdminListenersValidator(), ConfigDef.Importance.LOW, ADMIN_LISTENERS_DOC); } /** * Add the properties related to an internal-only server to the given {@link ConfigDef}. * @param configDef the {@link ConfigDef} to add the properties to; may not be null */ public static void addInternalConfig(ConfigDef configDef) { configDef .define( LISTENERS_CONFIG, ConfigDef.Type.LIST, LISTENERS_DEFAULT, new ListenersValidator(), ConfigDef.Importance.LOW, LISTENERS_DOC ).define( REST_ADVERTISED_HOST_NAME_CONFIG, ConfigDef.Type.STRING, null, ConfigDef.Importance.LOW, REST_ADVERTISED_HOST_NAME_DOC ).define( REST_ADVERTISED_PORT_CONFIG, ConfigDef.Type.INT, null, ConfigDef.Importance.LOW, REST_ADVERTISED_PORT_DOC ).define( REST_ADVERTISED_LISTENER_CONFIG, ConfigDef.Type.STRING, null, ConfigDef.Importance.LOW, REST_ADVERTISED_LISTENER_DOC ).define( ACCESS_CONTROL_ALLOW_ORIGIN_CONFIG, ConfigDef.Type.STRING, ACCESS_CONTROL_ALLOW_ORIGIN_DEFAULT, ConfigDef.Importance.LOW, ACCESS_CONTROL_ALLOW_ORIGIN_DOC ).define( ACCESS_CONTROL_ALLOW_METHODS_CONFIG, ConfigDef.Type.STRING, ACCESS_CONTROL_ALLOW_METHODS_DEFAULT, ConfigDef.Importance.LOW, ACCESS_CONTROL_ALLOW_METHODS_DOC ).define( RESPONSE_HTTP_HEADERS_CONFIG, ConfigDef.Type.STRING, RESPONSE_HTTP_HEADERS_DEFAULT, new ResponseHttpHeadersValidator(), ConfigDef.Importance.LOW, RESPONSE_HTTP_HEADERS_DOC ).define( BrokerSecurityConfigs.SSL_CLIENT_AUTH_CONFIG, ConfigDef.Type.STRING, BrokerSecurityConfigs.SSL_CLIENT_AUTH_DEFAULT, in(Utils.enumOptions(SslClientAuth.class)), ConfigDef.Importance.LOW, BrokerSecurityConfigs.SSL_CLIENT_AUTH_DOC); } public static RestServerConfig forPublic(Integer rebalanceTimeoutMs, Map props) { return new PublicConfig(rebalanceTimeoutMs, props); } public static RestServerConfig forInternal(Map props) { return new InternalConfig(props); } public List listeners() { return getList(LISTENERS_CONFIG); } public String rawListeners() { return (String) originals().get(LISTENERS_CONFIG); } public String allowedOrigins() { return getString(ACCESS_CONTROL_ALLOW_ORIGIN_CONFIG); } public String allowedMethods() { return getString(ACCESS_CONTROL_ALLOW_METHODS_CONFIG); } public String responseHeaders() { return getString(RESPONSE_HTTP_HEADERS_CONFIG); } public String advertisedListener() { return getString(RestServerConfig.REST_ADVERTISED_LISTENER_CONFIG); } public String advertisedHostName() { return getString(REST_ADVERTISED_HOST_NAME_CONFIG); } public Integer advertisedPort() { return getInt(REST_ADVERTISED_PORT_CONFIG); } public Integer rebalanceTimeoutMs() { return null; } protected RestServerConfig(ConfigDef configDef, Map props) { super(configDef, props, Utils.castToStringObjectMap(props), true); } // Visible for testing static void validateHttpResponseHeaderConfig(String config) { try { // validate format String[] configTokens = config.trim().split("\\s+", 2); if (configTokens.length != 2) { throw new ConfigException(String.format("Invalid format of header config '%s'. " + "Expected: '[action] [header name]:[header value]'", config)); } // validate action String method = configTokens[0].trim(); validateHeaderConfigAction(method); // validate header name and header value pair String header = configTokens[1]; String[] headerTokens = header.trim().split(":"); if (headerTokens.length != 2) { throw new ConfigException( String.format("Invalid format of header name and header value pair '%s'. " + "Expected: '[header name]:[header value]'", header)); } // validate header name String headerName = headerTokens[0].trim(); if (headerName.isEmpty() || headerName.matches(".*\\s+.*")) { throw new ConfigException(String.format("Invalid header name '%s'. " + "The '[header name]' cannot contain whitespace", headerName)); } } catch (ArrayIndexOutOfBoundsException e) { throw new ConfigException(String.format("Invalid header config '%s'.", config), e); } } // Visible for testing static void validateHeaderConfigAction(String action) { if (HEADER_ACTIONS.stream().noneMatch(action::equalsIgnoreCase)) { throw new ConfigException(String.format("Invalid header config action: '%s'. " + "Expected one of %s", action, HEADER_ACTIONS)); } } private static class ListenersValidator implements ConfigDef.Validator { @Override public void ensureValid(String name, Object value) { if (!(value instanceof List)) { throw new ConfigException("Invalid value type for listeners (expected list of URLs , ex: http://localhost:8080,https://localhost:8443)."); } List items = (List) value; if (items.isEmpty()) { throw new ConfigException("Invalid value for listeners, at least one URL is expected, ex: http://localhost:8080,https://localhost:8443."); } for (Object item : items) { if (!(item instanceof String)) { throw new ConfigException("Invalid type for listeners (expected String)."); } if (Utils.isBlank((String) item)) { throw new ConfigException("Empty URL found when parsing listeners list."); } } } @Override public String toString() { return "List of comma-separated URLs, ex: http://localhost:8080,https://localhost:8443."; } } private static class AdminListenersValidator implements ConfigDef.Validator { @Override public void ensureValid(String name, Object value) { if (value == null) { return; } if (!(value instanceof List)) { throw new ConfigException("Invalid value type for admin.listeners (expected list)."); } List items = (List) value; if (items.isEmpty()) { return; } for (Object item : items) { if (!(item instanceof String)) { throw new ConfigException("Invalid type for admin.listeners (expected String)."); } if (Utils.isBlank((String) item)) { throw new ConfigException("Empty URL found when parsing admin.listeners list."); } } } @Override public String toString() { return "List of comma-separated URLs, ex: http://localhost:8080,https://localhost:8443."; } } private static class ResponseHttpHeadersValidator implements ConfigDef.Validator { @Override public void ensureValid(String name, Object value) { String strValue = (String) value; if (Utils.isBlank(strValue)) { return; } String[] configs = StringUtil.csvSplit(strValue); // handles and removed surrounding quotes Arrays.stream(configs).forEach(RestServerConfig::validateHttpResponseHeaderConfig); } @Override public String toString() { return "Comma-separated header rules, where each header rule is of the form " + "'[action] [header name]:[header value]' and optionally surrounded by double quotes " + "if any part of a header rule contains a comma"; } } private static class InternalConfig extends RestServerConfig { private static ConfigDef config() { ConfigDef result = new ConfigDef().withClientSslSupport(); addInternalConfig(result); return result; } @Override public List adminListeners() { // Disable admin resources (such as the logging resource) return Collections.emptyList(); } @Override public List restExtensions() { // Disable the use of REST extensions return null; } @Override public boolean topicTrackingEnabled() { // Topic tracking is unnecessary if we don't expose a public REST API return false; } @Override public boolean topicTrackingResetEnabled() { // Topic tracking is unnecessary if we don't expose a public REST API return false; } public InternalConfig(Map props) { super(config(), props); } } private static class PublicConfig extends RestServerConfig { private final Integer rebalanceTimeoutMs; private static ConfigDef config() { ConfigDef result = new ConfigDef().withClientSslSupport(); addPublicConfig(result); WorkerConfig.addTopicTrackingConfig(result); return result; } @Override public List adminListeners() { return getList(ADMIN_LISTENERS_CONFIG); } @Override public List restExtensions() { return getList(REST_EXTENSION_CLASSES_CONFIG); } @Override public Integer rebalanceTimeoutMs() { return rebalanceTimeoutMs; } @Override public boolean topicTrackingEnabled() { return getBoolean(WorkerConfig.TOPIC_TRACKING_ENABLE_CONFIG); } @Override public boolean topicTrackingResetEnabled() { return getBoolean(WorkerConfig.TOPIC_TRACKING_ALLOW_RESET_CONFIG); } public PublicConfig(Integer rebalanceTimeoutMs, Map props) { super(config(), props); this.rebalanceTimeoutMs = rebalanceTimeoutMs; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy