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

com.linecorp.centraldogma.server.CentralDogmaConfig Maven / Gradle / Ivy

Go to download

Highly-available version-controlled service configuration repository based on Git, ZooKeeper and HTTP/2 (centraldogma-server)

There is a newer version: 0.70.0
Show newest version
/*
 * Copyright 2017 LINE Corporation
 *
 * LINE Corporation 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:
 *
 *   https://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 com.linecorp.centraldogma.server;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.linecorp.armeria.common.util.InetAddressPredicates.ofCidr;
import static com.linecorp.armeria.common.util.InetAddressPredicates.ofExact;
import static com.linecorp.armeria.server.ClientAddressSource.ofHeader;
import static com.linecorp.armeria.server.ClientAddressSource.ofProxyProtocol;
import static com.linecorp.centraldogma.server.CentralDogmaBuilder.DEFAULT_MAX_REMOVED_REPOSITORY_AGE_MILLIS;
import static com.linecorp.centraldogma.server.CentralDogmaBuilder.DEFAULT_NUM_REPOSITORY_WORKERS;
import static com.linecorp.centraldogma.server.CentralDogmaBuilder.DEFAULT_REPOSITORY_CACHE_SPEC;
import static com.linecorp.centraldogma.server.internal.storage.repository.RepositoryCache.validateCacheSpec;
import static java.util.Objects.requireNonNull;

import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cronutils.utils.VisibleForTesting;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.util.StdConverter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;

import com.linecorp.armeria.common.HttpHeaderNames;
import com.linecorp.armeria.common.SessionProtocol;
import com.linecorp.armeria.server.ClientAddressSource;
import com.linecorp.armeria.server.ServerPort;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.server.auth.AuthConfig;
import com.linecorp.centraldogma.server.plugin.PluginConfig;
import com.linecorp.centraldogma.server.plugin.PluginConfigDeserializer;
import com.linecorp.centraldogma.server.storage.repository.Repository;

import io.netty.util.NetUtil;

/**
 * {@link CentralDogma} server configuration.
 */
public final class CentralDogmaConfig {

    private static final Logger logger = LoggerFactory.getLogger(CentralDogmaConfig.class);

    private static final ObjectMapper objectMapper = new ObjectMapper();

    private static final Pattern PREFIX_PATTERN = Pattern.compile("^[a-z0-9_-]+$");

    private static final Map CONFIG_VALUE_CONVERTERS;

    static {
        final SimpleModule module = new SimpleModule();
        module.addDeserializer(PluginConfig.class, new PluginConfigDeserializer());
        // Use different ObjectMapper to avoid infinite recursion.
        objectMapper.registerModule(module);

        final ArrayList configValueConverters = new ArrayList<>();
        Streams.stream(ServiceLoader.load(ConfigValueConverter.class)).forEach(configValueConverters::add);
        configValueConverters.add(DefaultConfigValueConverter.INSTANCE);
        final ImmutableMap.Builder builder = ImmutableMap.builder();
        for (ConfigValueConverter configValueConverter : configValueConverters) {
            if (configValueConverter.supportedPrefixes().isEmpty()) {
                continue;
            }
            boolean addConverter = true;
            for (String supportedPrefix : configValueConverter.supportedPrefixes()) {
                if (!PREFIX_PATTERN.matcher(supportedPrefix).matches()) {
                    logger.warn("{} isn't used because it has an invalid prefix: {}. (expected: {})",
                                configValueConverter, supportedPrefix, PREFIX_PATTERN.pattern());
                    addConverter = false;
                    break;
                }
            }
            if (addConverter) {
                configValueConverter.supportedPrefixes()
                                    .forEach(prefix -> builder.put(prefix, configValueConverter));
            }
        }
        CONFIG_VALUE_CONVERTERS = ImmutableMap.copyOf(builder.buildOrThrow());

        final StringBuilder sb = new StringBuilder();
        sb.append('{');
        CONFIG_VALUE_CONVERTERS.entrySet().stream().sorted(Entry.comparingByKey()).forEach(
                entry -> sb.append(entry.getKey())
                           .append('=')
                           .append(entry.getValue().getClass().getName()).append(", "));
        sb.setLength(sb.length() - 2);
        sb.append('}');

        logger.debug("Available {}s: {}", ConfigValueConverter.class.getName(), sb);
    }

    /**
     * Converts the specified {@code value} using {@link ConfigValueConverter} if the specified {@code value}
     * starts with a prefix followed by a colon {@code ':'}.
     */
    @Nullable
    public static String convertValue(@Nullable String value, String propertyName) {
        if (value == null) {
            return null;
        }

        final int index = value.indexOf(':');
        if (index <= 0) {
            // no prefix or starts with ':'.
            return value;
        }

        final String prefix = value.substring(0, index);
        if (!PREFIX_PATTERN.matcher(prefix).matches()) {
            // Not a prefix.
            return value;
        }

        final String rest = value.substring(index + 1);

        final ConfigValueConverter converter = CONFIG_VALUE_CONVERTERS.get(prefix);
        if (converter != null) {
            return converter.convert(prefix, rest);
        }
        logger.warn("No {} found for {}. prefix: {}",
                    ConfigValueConverter.class.getSimpleName(), propertyName, prefix);
        return value;
    }

    /**
     * Loads the configuration from the specified {@link File}.
     */
    public static CentralDogmaConfig load(File configFile) throws JsonMappingException, JsonParseException {
        requireNonNull(configFile, "configFile");
        try {
            return objectMapper.readValue(configFile, CentralDogmaConfig.class);
        } catch (JsonParseException | JsonMappingException e) {
            throw e;
        } catch (IOException e) {
            throw new IOError(e);
        }
    }

    /**
     * Loads the configuration from the specified JSON string.
     */
    @VisibleForTesting
    public static CentralDogmaConfig load(String json) throws JsonMappingException, JsonParseException {
        requireNonNull(json, "json");
        try {
            return objectMapper.readValue(json, CentralDogmaConfig.class);
        } catch (JsonParseException | JsonMappingException e) {
            throw e;
        } catch (IOException e) {
            throw new IOError(e);
        }
    }

    private final File dataDir;

    // Armeria
    private final List ports;
    @Nullable
    private final Integer numWorkers;
    @Nullable
    private final Integer maxNumConnections;
    @Nullable
    private final Long requestTimeoutMillis;
    @Nullable
    private final Long idleTimeoutMillis;
    @Nullable
    private final Integer maxFrameLength;
    @Nullable
    private final TlsConfig tls;
    @Nullable
    private final List trustedProxyAddresses;
    @Nullable
    private final List clientAddressSources;

    private final Predicate trustedProxyAddressPredicate;
    private final List clientAddressSourceList;

    // Repository
    private final Integer numRepositoryWorkers;
    private final long maxRemovedRepositoryAgeMillis;

    // Cache
    private final String repositoryCacheSpec;

    // Web dashboard
    private final boolean webAppEnabled;

    @Nullable
    private final String webAppTitle;

    // Graceful shutdown
    @Nullable
    private final GracefulShutdownTimeout gracefulShutdownTimeout;

    // Replication
    private final ReplicationConfig replicationConfig;

    // Security
    private final boolean csrfTokenRequiredForThrift;

    // Access log
    @Nullable
    private final String accessLogFormat;

    @Nullable
    private final AuthConfig authConfig;

    @Nullable
    private final QuotaConfig writeQuotaPerRepository;

    @Nullable
    private final CorsConfig corsConfig;

    private final List pluginConfigs;
    private final Map, PluginConfig> pluginConfigMap;

    CentralDogmaConfig(
            @JsonProperty(value = "dataDir", required = true) File dataDir,
            @JsonProperty(value = "ports", required = true)
            @JsonDeserialize(contentUsing = ServerPortDeserializer.class)
                    List ports,
            @JsonProperty("tls") @Nullable TlsConfig tls,
            @JsonProperty("trustedProxyAddresses") @Nullable List trustedProxyAddresses,
            @JsonProperty("clientAddressSources") @Nullable List clientAddressSources,
            @JsonProperty("numWorkers") @Nullable Integer numWorkers,
            @JsonProperty("maxNumConnections") @Nullable Integer maxNumConnections,
            @JsonProperty("requestTimeoutMillis") @Nullable Long requestTimeoutMillis,
            @JsonProperty("idleTimeoutMillis") @Nullable Long idleTimeoutMillis,
            @JsonProperty("maxFrameLength") @Nullable Integer maxFrameLength,
            @JsonProperty("numRepositoryWorkers") @Nullable Integer numRepositoryWorkers,
            @JsonProperty("repositoryCacheSpec") @Nullable String repositoryCacheSpec,
            @JsonProperty("maxRemovedRepositoryAgeMillis") @Nullable Long maxRemovedRepositoryAgeMillis,
            @JsonProperty("gracefulShutdownTimeout") @Nullable GracefulShutdownTimeout gracefulShutdownTimeout,
            @JsonProperty("webAppEnabled") @Nullable Boolean webAppEnabled,
            @JsonProperty("webAppTitle") @Nullable String webAppTitle,
            @JsonProperty("replication") ReplicationConfig replicationConfig,
            @JsonProperty("csrfTokenRequiredForThrift") @Nullable Boolean csrfTokenRequiredForThrift,
            @JsonProperty("accessLogFormat") @Nullable String accessLogFormat,
            @JsonProperty("authentication") @Nullable AuthConfig authConfig,
            @JsonProperty("writeQuotaPerRepository") @Nullable QuotaConfig writeQuotaPerRepository,
            @JsonProperty("cors") @Nullable CorsConfig corsConfig,
            @JsonProperty("pluginConfigs") @Nullable List pluginConfigs) {

        this.dataDir = requireNonNull(dataDir, "dataDir");
        this.ports = ImmutableList.copyOf(requireNonNull(ports, "ports"));
        checkArgument(!ports.isEmpty(), "ports must have at least one port.");
        this.tls = tls;
        this.trustedProxyAddresses = trustedProxyAddresses;
        this.clientAddressSources = clientAddressSources;

        this.numWorkers = numWorkers;

        this.maxNumConnections = maxNumConnections;
        this.requestTimeoutMillis = requestTimeoutMillis;
        this.idleTimeoutMillis = idleTimeoutMillis;
        this.maxFrameLength = maxFrameLength;
        this.numRepositoryWorkers = firstNonNull(numRepositoryWorkers, DEFAULT_NUM_REPOSITORY_WORKERS);
        checkArgument(this.numRepositoryWorkers > 0,
                      "numRepositoryWorkers: %s (expected: > 0)", this.numRepositoryWorkers);
        this.maxRemovedRepositoryAgeMillis = firstNonNull(maxRemovedRepositoryAgeMillis,
                                                          DEFAULT_MAX_REMOVED_REPOSITORY_AGE_MILLIS);
        checkArgument(this.maxRemovedRepositoryAgeMillis >= 0,
                      "maxRemovedRepositoryAgeMillis: %s (expected: >= 0)", this.maxRemovedRepositoryAgeMillis);
        this.repositoryCacheSpec = validateCacheSpec(
                firstNonNull(repositoryCacheSpec, DEFAULT_REPOSITORY_CACHE_SPEC));

        this.webAppEnabled = firstNonNull(webAppEnabled, true);
        this.webAppTitle = webAppTitle;
        this.gracefulShutdownTimeout = gracefulShutdownTimeout;
        this.replicationConfig = firstNonNull(replicationConfig, ReplicationConfig.NONE);
        this.csrfTokenRequiredForThrift = firstNonNull(csrfTokenRequiredForThrift, true);
        this.accessLogFormat = accessLogFormat;

        this.authConfig = authConfig;

        final boolean hasTrustedProxyAddrCfg =
                trustedProxyAddresses != null && !trustedProxyAddresses.isEmpty();
        trustedProxyAddressPredicate =
                hasTrustedProxyAddrCfg ? toTrustedProxyAddressPredicate(trustedProxyAddresses)
                                       : addr -> false;
        clientAddressSourceList =
                toClientAddressSourceList(clientAddressSources, hasTrustedProxyAddrCfg,
                                          ports.stream().anyMatch(ServerPort::hasProxyProtocol));

        this.writeQuotaPerRepository = writeQuotaPerRepository;
        this.corsConfig = corsConfig;
        this.pluginConfigs = firstNonNull(pluginConfigs, ImmutableList.of());
        pluginConfigMap = this.pluginConfigs.stream().collect(
                toImmutableMap(PluginConfig::getClass, Function.identity()));
    }

    /**
     * Returns the data directory.
     */
    @JsonProperty
    public File dataDir() {
        return dataDir;
    }

    /**
     * Returns the {@link ServerPort}s.
     */
    @JsonProperty
    @JsonSerialize(contentUsing = ServerPortSerializer.class)
    public List ports() {
        return ports;
    }

    /**
     * Returns the TLS configuration.
     */
    @Nullable
    @JsonProperty
    public TlsConfig tls() {
        return tls;
    }

    /**
     * Returns the IP addresses of the trusted proxy servers. If trusted, the sources specified in
     * {@link #clientAddressSources()} will be used to determine the actual IP address of clients.
     */
    @Nullable
    @JsonProperty
    public List trustedProxyAddresses() {
        return trustedProxyAddresses;
    }

    /**
     * Returns the sources that determines a client address. For example:
     * 
    *
  • {@code "forwarded"}
  • *
  • {@code "x-forwarded-for"}
  • *
  • {@code "PROXY_PROTOCOL"}
  • *
* */ @Nullable @JsonProperty public List clientAddressSources() { return clientAddressSources; } /** * Returns the number of event loop threads. */ @JsonProperty @JsonSerialize(converter = OptionalConverter.class) public Optional numWorkers() { return Optional.ofNullable(numWorkers); } /** * Returns the maximum number of established connections. */ @JsonProperty @JsonSerialize(converter = OptionalConverter.class) public Optional maxNumConnections() { return Optional.ofNullable(maxNumConnections); } /** * Returns the request timeout in milliseconds. */ @JsonProperty @JsonSerialize(converter = OptionalConverter.class) public Optional requestTimeoutMillis() { return Optional.ofNullable(requestTimeoutMillis); } /** * Returns the timeout of an idle connection in milliseconds. */ @JsonProperty @JsonSerialize(converter = OptionalConverter.class) public Optional idleTimeoutMillis() { return Optional.ofNullable(idleTimeoutMillis); } /** * Returns the maximum length of request content in bytes. */ @JsonProperty @JsonSerialize(converter = OptionalConverter.class) public Optional maxFrameLength() { return Optional.ofNullable(maxFrameLength); } /** * Returns the number of repository worker threads. */ @JsonProperty int numRepositoryWorkers() { return numRepositoryWorkers; } /** * Returns the maximum age of a removed repository in milliseconds. A removed repository is first marked * as removed, and then is purged permanently once the amount of time returned by this property passes * since marked. */ @JsonProperty public long maxRemovedRepositoryAgeMillis() { return maxRemovedRepositoryAgeMillis; } /** * Returns the {@code repositoryCacheSpec}. * * @deprecated Use {@link #repositoryCacheSpec()}. */ @JsonProperty @Deprecated public String cacheSpec() { return repositoryCacheSpec; } /** * Returns the cache spec of the repository cache. */ @JsonProperty public String repositoryCacheSpec() { return repositoryCacheSpec; } /** * Returns the graceful shutdown timeout. */ @JsonProperty @JsonSerialize(converter = OptionalConverter.class) public Optional gracefulShutdownTimeout() { return Optional.ofNullable(gracefulShutdownTimeout); } /** * Returns whether web app is enabled. */ @JsonProperty public boolean isWebAppEnabled() { return webAppEnabled; } /** * Returns the title of the web app. */ @Nullable @JsonProperty("webAppTitle") public String webAppTitle() { return webAppTitle; } /** * Returns the {@link ReplicationConfig}. */ @JsonProperty("replication") public ReplicationConfig replicationConfig() { return replicationConfig; } /** * Returns whether a CSRF token is required for Thrift clients. Note that it's not safe to enable this * feature. It only exists for a legacy Thrift client that does not send a CSRF token. */ @JsonProperty public boolean isCsrfTokenRequiredForThrift() { return csrfTokenRequiredForThrift; } /** * Returns the access log format. */ @JsonProperty @Nullable public String accessLogFormat() { return accessLogFormat; } /** * Returns the {@link AuthConfig}. */ @Nullable @JsonProperty("authentication") public AuthConfig authConfig() { return authConfig; } /** * Returns the maximum allowed write quota per {@link Repository}. */ @Nullable @JsonProperty("writeQuotaPerRepository") public QuotaConfig writeQuotaPerRepository() { return writeQuotaPerRepository; } /** * Returns the {@link CorsConfig}. */ @Nullable @JsonProperty("cors") public CorsConfig corsConfig() { return corsConfig; } /** * Returns the list of {@link PluginConfig}s. */ @JsonProperty("pluginConfigs") public List pluginConfigs() { return pluginConfigs; } /** * Returns the map of {@link PluginConfig}s. */ public Map, PluginConfig> pluginConfigMap() { return pluginConfigMap; } @Override public String toString() { try { return Jackson.writeValueAsPrettyString(this); } catch (JsonProcessingException e) { throw new IllegalStateException(e); } } Predicate trustedProxyAddressPredicate() { return trustedProxyAddressPredicate; } List clientAddressSourceList() { return clientAddressSourceList; } private static Predicate toTrustedProxyAddressPredicate(List trustedProxyAddresses) { final String first = trustedProxyAddresses.get(0); Predicate predicate = first.indexOf('/') < 0 ? ofExact(first) : ofCidr(first); for (int i = 1; i < trustedProxyAddresses.size(); i++) { final String next = trustedProxyAddresses.get(i); predicate = predicate.or(next.indexOf('/') < 0 ? ofExact(next) : ofCidr(next)); } return predicate; } private static List toClientAddressSourceList( @Nullable List clientAddressSources, boolean useDefaultSources, boolean specifiedProxyProtocol) { if (clientAddressSources != null && !clientAddressSources.isEmpty()) { return clientAddressSources.stream().map( name -> "PROXY_PROTOCOL".equals(name) ? ofProxyProtocol() : ofHeader(name)) .collect(toImmutableList()); } if (useDefaultSources) { final Builder builder = new Builder<>(); builder.add(ofHeader(HttpHeaderNames.FORWARDED)); builder.add(ofHeader(HttpHeaderNames.X_FORWARDED_FOR)); if (specifiedProxyProtocol) { builder.add(ofProxyProtocol()); } return builder.build(); } return ImmutableList.of(); } static final class ServerPortSerializer extends JsonSerializer { @Override public void serialize(ServerPort value, JsonGenerator gen, SerializerProvider serializers) throws IOException { final InetSocketAddress localAddr = value.localAddress(); final int port = localAddr.getPort(); final String host; if (localAddr.getAddress().isAnyLocalAddress()) { host = "*"; } else { final String hs = localAddr.getHostString(); if (NetUtil.isValidIpV6Address(hs)) { // Try to get the platform-independent consistent IPv6 address string. host = NetUtil.toAddressString(localAddr.getAddress()); } else { host = hs; } } gen.writeStartObject(); gen.writeObjectFieldStart("localAddress"); gen.writeStringField("host", host); gen.writeNumberField("port", port); gen.writeEndObject(); gen.writeArrayFieldStart("protocols"); for (final SessionProtocol protocol : value.protocols()) { gen.writeString(protocol.uriText()); } gen.writeEndArray(); gen.writeEndObject(); } } static final class ServerPortDeserializer extends JsonDeserializer { @Override public ServerPort deserialize(JsonParser p, DeserializationContext ctx) throws IOException { final JsonNode root = p.getCodec().readTree(p); final JsonNode localAddress = root.get("localAddress"); if (localAddress == null || localAddress.getNodeType() != JsonNodeType.OBJECT) { return fail(ctx, root); } final JsonNode host = localAddress.get("host"); if (host == null || host.getNodeType() != JsonNodeType.STRING) { return fail(ctx, root); } final JsonNode port = localAddress.get("port"); if (port == null || port.getNodeType() != JsonNodeType.NUMBER) { return fail(ctx, root); } final ImmutableSet.Builder protocolsBuilder = ImmutableSet.builder(); final JsonNode protocols = root.get("protocols"); if (protocols != null) { if (protocols.getNodeType() != JsonNodeType.ARRAY) { return fail(ctx, root); } protocols.elements().forEachRemaining( protocol -> protocolsBuilder.add(SessionProtocol.of(protocol.textValue()))); } else { final JsonNode protocol = root.get("protocol"); if (protocol == null || protocol.getNodeType() != JsonNodeType.STRING) { return fail(ctx, root); } protocolsBuilder.add(SessionProtocol.of(protocol.textValue())); } final String hostVal = host.textValue(); final int portVal = port.intValue(); final InetSocketAddress localAddressVal; if ("*".equals(hostVal)) { localAddressVal = new InetSocketAddress(portVal); } else { localAddressVal = new InetSocketAddress(hostVal, portVal); } return new ServerPort(localAddressVal, protocolsBuilder.build()); } private static ServerPort fail(DeserializationContext ctx, JsonNode root) throws JsonMappingException { ctx.reportInputMismatch(ServerPort.class, "invalid server port information: %s", root); throw new Error(); // Should never reach here. } } static final class OptionalConverter extends StdConverter, Object> { @Override @Nullable public Object convert(Optional value) { return value.orElse(null); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy