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

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

Go to download

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

The 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.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.API_V0_PATH_PREFIX;
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.API_V1_PATH_PREFIX;
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.HEALTH_CHECK_PATH;
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.METRICS_PATH;
import static com.linecorp.centraldogma.server.auth.AuthProvider.LOGIN_API_ROUTES;
import static com.linecorp.centraldogma.server.auth.AuthProvider.LOGIN_PATH;
import static com.linecorp.centraldogma.server.auth.AuthProvider.LOGOUT_API_ROUTES;
import static com.linecorp.centraldogma.server.auth.AuthProvider.LOGOUT_PATH;
import static java.util.Objects.requireNonNull;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

import javax.annotation.Nullable;

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

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import com.linecorp.armeria.common.DependencyInjector;
import com.linecorp.armeria.common.Flags;
import com.linecorp.armeria.common.HttpData;
import com.linecorp.armeria.common.HttpHeaderNames;
import com.linecorp.armeria.common.HttpHeaders;
import com.linecorp.armeria.common.HttpMethod;
import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.common.ServerCacheControl;
import com.linecorp.armeria.common.SessionProtocol;
import com.linecorp.armeria.common.metric.MeterIdPrefixFunction;
import com.linecorp.armeria.common.prometheus.PrometheusMeterRegistries;
import com.linecorp.armeria.common.util.EventLoopGroups;
import com.linecorp.armeria.common.util.Exceptions;
import com.linecorp.armeria.common.util.StartStopSupport;
import com.linecorp.armeria.common.util.SystemInfo;
import com.linecorp.armeria.internal.common.ReflectiveDependencyInjector;
import com.linecorp.armeria.server.AbstractHttpService;
import com.linecorp.armeria.server.ContextPathServicesBuilder;
import com.linecorp.armeria.server.DecoratingServiceBindingBuilder;
import com.linecorp.armeria.server.HttpService;
import com.linecorp.armeria.server.Route;
import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.ServerPort;
import com.linecorp.armeria.server.ServiceNaming;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.annotation.JacksonRequestConverterFunction;
import com.linecorp.armeria.server.auth.AuthService;
import com.linecorp.armeria.server.auth.Authorizer;
import com.linecorp.armeria.server.cors.CorsService;
import com.linecorp.armeria.server.docs.DocService;
import com.linecorp.armeria.server.encoding.DecodingService;
import com.linecorp.armeria.server.encoding.EncodingService;
import com.linecorp.armeria.server.file.FileService;
import com.linecorp.armeria.server.file.HttpFile;
import com.linecorp.armeria.server.healthcheck.HealthCheckService;
import com.linecorp.armeria.server.healthcheck.SettableHealthChecker;
import com.linecorp.armeria.server.logging.AccessLogWriter;
import com.linecorp.armeria.server.management.ManagementService;
import com.linecorp.armeria.server.metric.MetricCollectingService;
import com.linecorp.armeria.server.prometheus.PrometheusExpositionService;
import com.linecorp.armeria.server.thrift.THttpService;
import com.linecorp.armeria.server.thrift.ThriftCallService;
import com.linecorp.centraldogma.common.ShuttingDownException;
import com.linecorp.centraldogma.internal.CsrfToken;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.internal.thrift.CentralDogmaService;
import com.linecorp.centraldogma.server.auth.AuthConfig;
import com.linecorp.centraldogma.server.auth.AuthProvider;
import com.linecorp.centraldogma.server.auth.AuthProviderParameters;
import com.linecorp.centraldogma.server.auth.SessionManager;
import com.linecorp.centraldogma.server.command.Command;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.command.StandaloneCommandExecutor;
import com.linecorp.centraldogma.server.internal.admin.auth.CachedSessionManager;
import com.linecorp.centraldogma.server.internal.admin.auth.CsrfTokenAuthorizer;
import com.linecorp.centraldogma.server.internal.admin.auth.ExpiredSessionDeletingSessionManager;
import com.linecorp.centraldogma.server.internal.admin.auth.FileBasedSessionManager;
import com.linecorp.centraldogma.server.internal.admin.auth.SessionTokenAuthorizer;
import com.linecorp.centraldogma.server.internal.admin.service.DefaultLogoutService;
import com.linecorp.centraldogma.server.internal.admin.service.RepositoryService;
import com.linecorp.centraldogma.server.internal.admin.service.UserService;
import com.linecorp.centraldogma.server.internal.api.AdministrativeService;
import com.linecorp.centraldogma.server.internal.api.ContentServiceV1;
import com.linecorp.centraldogma.server.internal.api.CredentialServiceV1;
import com.linecorp.centraldogma.server.internal.api.GitHttpService;
import com.linecorp.centraldogma.server.internal.api.HttpApiExceptionHandler;
import com.linecorp.centraldogma.server.internal.api.MetadataApiService;
import com.linecorp.centraldogma.server.internal.api.MirroringServiceV1;
import com.linecorp.centraldogma.server.internal.api.ProjectServiceV1;
import com.linecorp.centraldogma.server.internal.api.RepositoryServiceV1;
import com.linecorp.centraldogma.server.internal.api.TokenService;
import com.linecorp.centraldogma.server.internal.api.WatchService;
import com.linecorp.centraldogma.server.internal.api.auth.ApplicationTokenAuthorizer;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresPermissionDecorator.RequiresReadPermissionDecoratorFactory;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresPermissionDecorator.RequiresWritePermissionDecoratorFactory;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresRoleDecorator.RequiresRoleDecoratorFactory;
import com.linecorp.centraldogma.server.internal.api.converter.HttpApiRequestConverter;
import com.linecorp.centraldogma.server.internal.mirror.DefaultMirroringServicePlugin;
import com.linecorp.centraldogma.server.internal.replication.ZooKeeperCommandExecutor;
import com.linecorp.centraldogma.server.internal.storage.project.DefaultProjectManager;
import com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager;
import com.linecorp.centraldogma.server.internal.storage.repository.MirrorConfig;
import com.linecorp.centraldogma.server.internal.thrift.CentralDogmaExceptionTranslator;
import com.linecorp.centraldogma.server.internal.thrift.CentralDogmaServiceImpl;
import com.linecorp.centraldogma.server.internal.thrift.CentralDogmaTimeoutScheduler;
import com.linecorp.centraldogma.server.internal.thrift.TokenlessClientLogger;
import com.linecorp.centraldogma.server.management.ServerStatus;
import com.linecorp.centraldogma.server.management.ServerStatusManager;
import com.linecorp.centraldogma.server.metadata.MetadataService;
import com.linecorp.centraldogma.server.mirror.MirrorProvider;
import com.linecorp.centraldogma.server.plugin.AllReplicasPlugin;
import com.linecorp.centraldogma.server.plugin.Plugin;
import com.linecorp.centraldogma.server.plugin.PluginInitContext;
import com.linecorp.centraldogma.server.plugin.PluginTarget;
import com.linecorp.centraldogma.server.storage.project.InternalProjectInitializer;
import com.linecorp.centraldogma.server.storage.project.ProjectManager;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics;
import io.micrometer.core.instrument.binder.jvm.DiskSpaceMetrics;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import io.micrometer.core.instrument.binder.system.UptimeMetrics;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * Central Dogma server.
 *
 * @see CentralDogmaBuilder
 */
public class CentralDogma implements AutoCloseable {

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

    private static final boolean GIT_MIRROR_ENABLED;

    static {
        Jackson.registerModules(new SimpleModule().addSerializer(CacheStats.class, new CacheStatsSerializer()));

        boolean gitMirrorEnabled = false;
        for (MirrorProvider mirrorProvider : MirrorConfig.MIRROR_PROVIDERS) {
            if ("com.linecorp.centraldogma.server.internal.mirror.GitMirrorProvider"
                    .equals(mirrorProvider.getClass().getName())) {
                gitMirrorEnabled = true;
                break;
            }
        }
        logger.info("Git mirroring: {}",
                    gitMirrorEnabled ? "enabled"
                                     : "disabled ('centraldogma-server-mirror-git' module is not available)");
        GIT_MIRROR_ENABLED = gitMirrorEnabled;
    }

    /**
     * Creates a new instance from the given configuration file.
     *
     * @throws IOException if failed to load the configuration from the specified file
     */
    public static CentralDogma forConfig(File configFile) throws IOException {
        requireNonNull(configFile, "configFile");
        return new CentralDogma(CentralDogmaConfig.load(configFile), Flags.meterRegistry());
    }

    private final SettableHealthChecker serverHealth = new SettableHealthChecker(false);
    private final CentralDogmaStartStop startStop;

    private final AtomicInteger numPendingStopRequests = new AtomicInteger();

    @Nullable
    private final PluginGroup pluginsForAllReplicas;
    @Nullable
    private final PluginGroup pluginsForLeaderOnly;

    private final CentralDogmaConfig cfg;
    @Nullable
    private volatile ProjectManager pm;
    @Nullable
    private volatile Server server;
    @Nullable
    private ExecutorService repositoryWorker;
    @Nullable
    private ScheduledExecutorService purgeWorker;
    @Nullable
    private CommandExecutor executor;
    private final MeterRegistry meterRegistry;
    @Nullable
    MeterRegistry meterRegistryToBeClosed;
    @Nullable
    private SessionManager sessionManager;
    @Nullable
    private ServerStatusManager statusManager;
    @Nullable
    private InternalProjectInitializer projectInitializer;

    CentralDogma(CentralDogmaConfig cfg, MeterRegistry meterRegistry) {
        this.cfg = requireNonNull(cfg, "cfg");
        pluginsForAllReplicas = PluginGroup.loadPlugins(
                CentralDogma.class.getClassLoader(), PluginTarget.ALL_REPLICAS, cfg);
        pluginsForLeaderOnly = PluginGroup.loadPlugins(
                CentralDogma.class.getClassLoader(), PluginTarget.LEADER_ONLY, cfg);
        startStop = new CentralDogmaStartStop(pluginsForAllReplicas);
        this.meterRegistry = meterRegistry;
    }

    /**
     * Returns the configuration of the server.
     *
     * @return the {@link CentralDogmaConfig} instance which is used for configuring this {@link CentralDogma}.
     */
    public CentralDogmaConfig config() {
        return cfg;
    }

    /**
     * Returns the primary port of the server.
     *
     * @return the primary {@link ServerPort} if the server is started. {@link Optional#empty()} otherwise.
     */
    @Nullable
    public ServerPort activePort() {
        final Server server = this.server;
        return server != null ? server.activePort() : null;
    }

    /**
     * Returns the ports of the server.
     *
     * @return the {@link Map} which contains the pairs of local {@link InetSocketAddress} and
     *         {@link ServerPort} is the server is started. {@link Optional#empty()} otherwise.
     */
    public Map activePorts() {
        final Server server = this.server;
        if (server != null) {
            return server.activePorts();
        } else {
            return Collections.emptyMap();
        }
    }

    /**
     * Returns the {@link ProjectManager} of the server if the server is started.
     * {@code null} is returned, otherwise.
     */
    @Nullable
    public ProjectManager projectManager() {
        return pm;
    }

    /**
     * Returns the {@link MirroringService} of the server.
     *
     * @return the {@link MirroringService} if the server is started and mirroring is enabled.
     *         {@link Optional#empty()} otherwise.
     */
    public Optional mirroringService() {
        if (pluginsForLeaderOnly == null) {
            return Optional.empty();
        }
        return pluginsForLeaderOnly.findFirstPlugin(DefaultMirroringServicePlugin.class)
                                   .map(DefaultMirroringServicePlugin::mirroringService);
    }

    /**
     * Returns the {@link Plugin}s which have been loaded.
     *
     * @param target the {@link PluginTarget} of the {@link Plugin}s to be returned
     */
    public List plugins(PluginTarget target) {
        switch (requireNonNull(target, "target")) {
            case LEADER_ONLY:
                return pluginsForLeaderOnly != null ? ImmutableList.copyOf(pluginsForLeaderOnly.plugins())
                                                    : ImmutableList.of();
            case ALL_REPLICAS:
                return pluginsForAllReplicas != null ? ImmutableList.copyOf(pluginsForAllReplicas.plugins())
                                                     : ImmutableList.of();
            default:
                // Should not reach here.
                throw new Error("Unknown plugin target: " + target);
        }
    }

    /**
     * Returns the {@link MeterRegistry} that contains the stats related with the server.
     */
    public Optional meterRegistry() {
        return Optional.ofNullable(meterRegistry);
    }

    /**
     * Starts the server.
     */
    public CompletableFuture start() {
        return startStop.start(true);
    }

    /**
     * Stops the server. This method does nothing if the server is stopped already.
     */
    public CompletableFuture stop() {
        serverHealth.setHealthy(false);

        final Optional gracefulTimeoutOpt = cfg.gracefulShutdownTimeout();
        if (gracefulTimeoutOpt.isPresent()) {
            try {
                // Sleep 1 second so that clients have some time to redirect traffic according
                // to the health status
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                logger.debug("Interrupted while waiting for quiet period", e);
                Thread.currentThread().interrupt();
            }
        }

        numPendingStopRequests.incrementAndGet();
        return startStop.stop().thenRun(numPendingStopRequests::decrementAndGet);
    }

    @Override
    public void close() {
        startStop.close();
    }

    private void doStart() throws Exception {
        boolean success = false;
        ExecutorService repositoryWorker = null;
        ScheduledExecutorService purgeWorker = null;
        ProjectManager pm = null;
        CommandExecutor executor = null;
        Server server = null;
        SessionManager sessionManager = null;
        try {
            logger.info("Starting the Central Dogma ..");

            final ThreadPoolExecutor repositoryWorkerImpl = new ThreadPoolExecutor(
                    cfg.numRepositoryWorkers(), cfg.numRepositoryWorkers(),
                    // TODO(minwoox): Use LinkedTransferQueue when we upgrade to JDK 21.
                    60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
                    new DefaultThreadFactory("repository-worker", true));
            repositoryWorkerImpl.allowCoreThreadTimeOut(true);
            repositoryWorker = ExecutorServiceMetrics.monitor(meterRegistry, repositoryWorkerImpl,
                                                              "repositoryWorker");

            logger.info("Starting the project manager: {}", cfg.dataDir());

            purgeWorker = Executors.newSingleThreadScheduledExecutor(
                    new DefaultThreadFactory("purge-worker", true));

            pm = new DefaultProjectManager(cfg.dataDir(), repositoryWorker, purgeWorker,
                                           meterRegistry, cfg.repositoryCacheSpec());

            logger.info("Started the project manager: {}", pm);

            logger.info("Current settings:\n{}", cfg);

            sessionManager = initializeSessionManager();

            logger.info("Starting the command executor ..");
            executor = startCommandExecutor(pm, repositoryWorker, purgeWorker,
                                            meterRegistry, sessionManager);
            // The projectInitializer is set in startCommandExecutor.
            assert projectInitializer != null;
            if (executor.isWritable()) {
                logger.info("Started the command executor.");
            }

            logger.info("Starting the RPC server.");
            server = startServer(pm, executor, purgeWorker, meterRegistry, sessionManager,
                                 projectInitializer);
            logger.info("Started the RPC server at: {}", server.activePorts());
            logger.info("Started the Central Dogma successfully.");
            success = true;
        } finally {
            if (success) {
                serverHealth.setHealthy(true);
                this.repositoryWorker = repositoryWorker;
                this.purgeWorker = purgeWorker;
                this.pm = pm;
                this.executor = executor;
                this.server = server;
                this.sessionManager = sessionManager;
            } else {
                doStop(server, executor, pm, repositoryWorker, purgeWorker, sessionManager);
            }
        }
    }

    private CommandExecutor startCommandExecutor(
            ProjectManager pm, Executor repositoryWorker,
            ScheduledExecutorService purgeWorker, MeterRegistry meterRegistry,
            @Nullable SessionManager sessionManager) {

        final Consumer onTakeLeadership = exec -> {
            if (pluginsForLeaderOnly != null) {
                logger.info("Starting plugins on the leader replica ..");
                pluginsForLeaderOnly
                        .start(cfg, pm, exec, meterRegistry, purgeWorker, projectInitializer)
                        .handle((unused, cause) -> {
                            if (cause == null) {
                                logger.info("Started plugins on the leader replica.");
                            } else {
                                logger.error("Failed to start plugins on the leader replica..", cause);
                            }
                            return null;
                        });
            }
        };

        final Consumer onReleaseLeadership = exec -> {
            if (pluginsForLeaderOnly != null) {
                logger.info("Stopping plugins on the leader replica ..");
                pluginsForLeaderOnly.stop(cfg, pm, exec, meterRegistry, purgeWorker, projectInitializer)
                                    .handle((unused, cause) -> {
                                        if (cause == null) {
                                            logger.info("Stopped plugins on the leader replica.");
                                        } else {
                                            logger.error("Failed to stop plugins on the leader replica.",
                                                         cause);
                                        }
                                        return null;
                                    });
            }
        };

        statusManager = new ServerStatusManager(cfg.dataDir());
        logger.info("Startup mode: {}", statusManager.serverStatus());
        final CommandExecutor executor;
        final ReplicationMethod replicationMethod = cfg.replicationConfig().method();
        switch (replicationMethod) {
            case ZOOKEEPER:
                executor = newZooKeeperCommandExecutor(pm, repositoryWorker, statusManager, meterRegistry,
                                                       sessionManager, onTakeLeadership, onReleaseLeadership);
                break;
            case NONE:
                logger.info("No replication mechanism specified; entering standalone");
                executor = new StandaloneCommandExecutor(pm, repositoryWorker, statusManager, sessionManager,
                                                         cfg.writeQuotaPerRepository(),
                                                         onTakeLeadership, onReleaseLeadership);
                break;
            default:
                throw new Error("unknown replication method: " + replicationMethod);
        }
        projectInitializer = new InternalProjectInitializer(executor, pm);

        final ServerStatus initialServerStatus = statusManager.serverStatus();
        executor.setWritable(initialServerStatus.writable());
        if (!initialServerStatus.replicating()) {
            projectInitializer.whenInitialized().complete(null);
            return executor;
        }
        try {
            final CompletableFuture startFuture = executor.start();
            while (!startFuture.isDone()) {
                if (numPendingStopRequests.get() > 0) {
                    // Stop request has been issued.
                    executor.stop().get();
                    break;
                }

                try {
                    startFuture.get(100, TimeUnit.MILLISECONDS);
                } catch (TimeoutException unused) {
                    // Taking long time ..
                }
            }

            // Trigger the exception if any.
            startFuture.get();
            projectInitializer.initialize();
        } catch (Exception e) {
            projectInitializer.whenInitialized().complete(null);
            logger.warn("Failed to start the command executor. Entering read-only.", e);
        }

        return executor;
    }

    @Nullable
    private SessionManager initializeSessionManager() throws Exception {
        final AuthConfig authCfg = cfg.authConfig();
        if (authCfg == null) {
            return null;
        }

        boolean success = false;
        SessionManager manager = null;
        try {
            manager = new FileBasedSessionManager(new File(cfg.dataDir(), "_sessions").toPath(),
                                                  authCfg.sessionValidationSchedule());
            manager = new CachedSessionManager(manager, Caffeine.from(authCfg.sessionCacheSpec()).build());
            manager = new ExpiredSessionDeletingSessionManager(manager);
            success = true;
            return manager;
        } finally {
            if (!success && manager != null) {
                try {
                    // It will eventually close FileBasedSessionManager because the other managers just forward
                    // the close method call to their delegate.
                    manager.close();
                } catch (Exception e) {
                    logger.warn("Failed to close a session manager.", e);
                }
            }
        }
    }

    private Server startServer(ProjectManager pm, CommandExecutor executor,
                               ScheduledExecutorService purgeWorker, MeterRegistry meterRegistry,
                               @Nullable SessionManager sessionManager,
                               InternalProjectInitializer projectInitializer) {
        final ServerBuilder sb = Server.builder();
        sb.verboseResponses(true);
        cfg.ports().forEach(sb::port);

        final boolean needsTls =
                cfg.ports().stream().anyMatch(ServerPort::hasTls) ||
                (cfg.managementConfig() != null && cfg.managementConfig().protocol().isTls());

        if (needsTls) {
            try {
                final TlsConfig tlsConfig = cfg.tls();
                if (tlsConfig != null) {
                    try (InputStream keyCertChainInputStream = tlsConfig.keyCertChainInputStream();
                         InputStream keyInputStream = tlsConfig.keyInputStream()) {
                        sb.tls(keyCertChainInputStream, keyInputStream, tlsConfig.keyPassword());
                    }
                } else {
                    logger.warn(
                            "Missing TLS configuration. Generating a self-signed certificate for TLS support.");
                    sb.tlsSelfSigned();
                }
            } catch (Exception e) {
                Exceptions.throwUnsafely(e);
            }
        }

        sb.clientAddressSources(cfg.clientAddressSourceList());
        sb.clientAddressTrustedProxyFilter(cfg.trustedProxyAddressPredicate());

        cfg.numWorkers().ifPresent(
                numWorkers -> sb.workerGroup(EventLoopGroups.newEventLoopGroup(numWorkers), true));
        cfg.maxNumConnections().ifPresent(sb::maxNumConnections);
        cfg.idleTimeoutMillis().ifPresent(sb::idleTimeoutMillis);
        cfg.requestTimeoutMillis().ifPresent(sb::requestTimeoutMillis);
        cfg.maxFrameLength().ifPresent(sb::maxRequestLength);
        cfg.gracefulShutdownTimeout().ifPresent(
                t -> sb.gracefulShutdownTimeoutMillis(t.quietPeriodMillis(), t.timeoutMillis()));

        final MetadataService mds = new MetadataService(pm, executor);
        final WatchService watchService = new WatchService(meterRegistry);
        final AuthProvider authProvider = createAuthProvider(executor, sessionManager, mds);
        final ProjectApiManager projectApiManager = new ProjectApiManager(pm, executor, mds);

        configureThriftService(sb, projectApiManager, executor, watchService, mds);

        sb.service("/title", webAppTitleFile(cfg.webAppTitle(), SystemInfo.hostname()).asService());

        sb.service(HEALTH_CHECK_PATH, HealthCheckService.builder()
                                                        .checkers(serverHealth)
                                                        .build());
        configManagement(sb, config().managementConfig());

        sb.serviceUnder("/docs/",
                        DocService.builder()
                                  .exampleHeaders(CentralDogmaService.class,
                                                  HttpHeaders.of(HttpHeaderNames.AUTHORIZATION,
                                                                 "Bearer " + CsrfToken.ANONYMOUS))
                                  .build());
        final Function authService =
                authService(mds, authProvider, sessionManager);
        configureHttpApi(sb, projectApiManager, executor, watchService, mds, authProvider, authService,
                         meterRegistry);

        configureMetrics(sb, meterRegistry);
        // Add the CORS service as the last decorator(executed first) so that the CORS service is applied
        // before AuthService.
        configCors(sb, config().corsConfig());

        // Configure access log format.
        final String accessLogFormat = cfg.accessLogFormat();
        if (isNullOrEmpty(accessLogFormat)) {
            sb.accessLogWriter(AccessLogWriter.disabled(), true);
        } else if ("common".equals(accessLogFormat)) {
            sb.accessLogWriter(AccessLogWriter.common(), true);
        } else if ("combined".equals(accessLogFormat)) {
            sb.accessLogWriter(AccessLogWriter.combined(), true);
        } else {
            sb.accessLogFormat(accessLogFormat);
        }

        if (pluginsForAllReplicas != null) {
            final PluginInitContext pluginInitContext =
                    new PluginInitContext(config(), pm, executor, meterRegistry, purgeWorker, sb,
                                          authService, projectInitializer);
            pluginsForAllReplicas.plugins()
                                 .forEach(p -> {
                                     if (!(p instanceof AllReplicasPlugin)) {
                                         return;
                                     }
                                     final AllReplicasPlugin plugin = (AllReplicasPlugin) p;
                                     plugin.init(pluginInitContext);
                                 });
        }
        // Configure the uncaught exception handler just before starting the server so that override the
        // default exception handler set by third-party libraries such as NIOServerCnxnFactory.
        Thread.setDefaultUncaughtExceptionHandler((t, e) -> logger.warn("Uncaught exception: {}", t, e));

        final Server s = sb.build();
        s.start().join();
        return s;
    }

    static HttpFile webAppTitleFile(@Nullable String webAppTitle, String hostname) {
        requireNonNull(hostname, "hostname");
        final Map titleAndHostname = ImmutableMap.of(
                "title", firstNonNull(webAppTitle, "Central Dogma at {{hostname}}"),
                "hostname", hostname);

        try {
            final HttpData data = HttpData.ofUtf8(Jackson.writeValueAsString(titleAndHostname));
            return HttpFile.builder(data)
                           .contentType(MediaType.JSON_UTF_8)
                           .cacheControl(ServerCacheControl.REVALIDATED)
                           .build();
        } catch (JsonProcessingException e) {
            throw new Error("Failed to encode the title and hostname:", e);
        }
    }

    @Nullable
    private AuthProvider createAuthProvider(
            CommandExecutor commandExecutor, @Nullable SessionManager sessionManager, MetadataService mds) {
        final AuthConfig authCfg = cfg.authConfig();
        if (authCfg == null) {
            return null;
        }

        checkState(sessionManager != null, "SessionManager is null");
        final AuthProviderParameters parameters = new AuthProviderParameters(
                // Find application first, then find the session token.
                new ApplicationTokenAuthorizer(mds::findTokenBySecret).orElse(
                        new SessionTokenAuthorizer(sessionManager, authCfg.administrators())),
                cfg,
                sessionManager::generateSessionId,
                // Propagate login and logout events to the other replicas.
                session -> commandExecutor.execute(Command.createSession(session)),
                sessionId -> commandExecutor.execute(Command.removeSession(sessionId)));
        return authCfg.factory().create(parameters);
    }

    private CommandExecutor newZooKeeperCommandExecutor(
            ProjectManager pm, Executor repositoryWorker,
            ServerStatusManager serverStatusManager,
            MeterRegistry meterRegistry,
            @Nullable SessionManager sessionManager,
            @Nullable Consumer onTakeLeadership,
            @Nullable Consumer onReleaseLeadership) {
        final ZooKeeperReplicationConfig zkCfg = (ZooKeeperReplicationConfig) cfg.replicationConfig();

        // Delete the old UUID replica ID which is not used anymore.
        final File dataDir = cfg.dataDir();
        new File(dataDir, "replica_id").delete();

        // TODO(trustin): Provide a way to restart/reload the replicator
        //                so that we can recover from ZooKeeper maintenance automatically.
        return new ZooKeeperCommandExecutor(
                zkCfg, dataDir,
                new StandaloneCommandExecutor(pm, repositoryWorker, serverStatusManager, sessionManager,
                        /* onTakeLeadership */ null, /* onReleaseLeadership */ null),
                meterRegistry, pm, config().writeQuotaPerRepository(), onTakeLeadership, onReleaseLeadership);
    }

    private void configureThriftService(ServerBuilder sb, ProjectApiManager projectApiManager,
                                        CommandExecutor executor,
                                        WatchService watchService, MetadataService mds) {
        final CentralDogmaServiceImpl service =
                new CentralDogmaServiceImpl(projectApiManager, executor, watchService, mds);

        HttpService thriftService =
                ThriftCallService.of(service)
                                 .decorate(CentralDogmaTimeoutScheduler::new)
                                 .decorate(CentralDogmaExceptionTranslator::new)
                                 .decorate(THttpService.newDecorator());

        if (cfg.isCsrfTokenRequiredForThrift()) {
            thriftService = thriftService.decorate(AuthService.newDecorator(new CsrfTokenAuthorizer()));
        } else {
            thriftService = thriftService.decorate(TokenlessClientLogger::new);
        }

        // Enable content compression for API responses.
        thriftService = thriftService.decorate(contentEncodingDecorator());

        sb.service("/cd/thrift/v1", thriftService);
    }

    private Function authService(
            MetadataService mds, @Nullable AuthProvider authProvider, @Nullable SessionManager sessionManager) {
        if (authProvider == null) {
            return AuthService.newDecorator(new CsrfTokenAuthorizer());
        }
        final AuthConfig authCfg = cfg.authConfig();
        assert authCfg != null : "authCfg";
        assert sessionManager != null : "sessionManager";
        final Authorizer tokenAuthorizer =
                new ApplicationTokenAuthorizer(mds::findTokenBySecret)
                        .orElse(new SessionTokenAuthorizer(sessionManager,
                                                           authCfg.administrators()));
        return AuthService.builder()
                          .add(tokenAuthorizer)
                          .onFailure(new CentralDogmaAuthFailureHandler())
                          .newDecorator();
    }

    private void configureHttpApi(ServerBuilder sb,
                                  ProjectApiManager projectApiManager, CommandExecutor executor,
                                  WatchService watchService, MetadataService mds,
                                  @Nullable AuthProvider authProvider,
                                  Function authService,
                                  MeterRegistry meterRegistry) {
        final DependencyInjector dependencyInjector = DependencyInjector.ofSingletons(
                // Use the default ObjectMapper without any configuration.
                // See JacksonRequestConverterFunctionTest
                new JacksonRequestConverterFunction(new ObjectMapper()),
                new HttpApiRequestConverter(projectApiManager),
                new RequiresReadPermissionDecoratorFactory(mds),
                new RequiresWritePermissionDecoratorFactory(mds),
                new RequiresRoleDecoratorFactory(mds)
        );
        sb.dependencyInjector(dependencyInjector, false)
          // TODO(ikhoon): Consider exposing ReflectiveDependencyInjector as a public API via
          //               DependencyInjector.ofReflective()
          .dependencyInjector(new ReflectiveDependencyInjector(), false);

        // Enable content compression for API responses.
        final Function decorator =
                authService.andThen(contentEncodingDecorator());
        for (String path : ImmutableList.of(API_V0_PATH_PREFIX, API_V1_PATH_PREFIX)) {
            final DecoratingServiceBindingBuilder decoratorBuilder =
                    sb.routeDecorator().pathPrefix(path);
            for (Route loginRoute : LOGIN_API_ROUTES) {
                decoratorBuilder.exclude(loginRoute);
            }
            for (Route logoutRoute : LOGOUT_API_ROUTES) {
                decoratorBuilder.exclude(logoutRoute);
            }
            decoratorBuilder.build(decorator);
        }

        assert statusManager != null;
        final ContextPathServicesBuilder apiV1ServiceBuilder = sb.contextPath(API_V1_PATH_PREFIX);
        apiV1ServiceBuilder
                .annotatedService(new AdministrativeService(executor, statusManager))
                .annotatedService(new ProjectServiceV1(projectApiManager, executor))
                .annotatedService(new RepositoryServiceV1(executor, mds));

        if (GIT_MIRROR_ENABLED) {
            apiV1ServiceBuilder.annotatedService(new MirroringServiceV1(projectApiManager, executor))
                               .annotatedService(new CredentialServiceV1(projectApiManager, executor));
        }

        apiV1ServiceBuilder.annotatedService()
                           .defaultServiceNaming(new ServiceNaming() {
                               private final String serviceName = ContentServiceV1.class.getName();
                               private final String watchServiceName =
                                       serviceName.replace("ContentServiceV1", "WatchContentServiceV1");

                               @Override
                               public String serviceName(ServiceRequestContext ctx) {
                                   if (ctx.request().headers().contains(HttpHeaderNames.IF_NONE_MATCH)) {
                                       return watchServiceName;
                                   }
                                   return serviceName;
                               }
                           })
                           .build(new ContentServiceV1(executor, watchService, meterRegistry));

        if (authProvider != null) {
            sb.service("/security_enabled", new AbstractHttpService() {
                @Override
                protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
                    return HttpResponse.of(HttpStatus.OK);
                }
            });

            final AuthConfig authCfg = cfg.authConfig();
            assert authCfg != null : "authCfg";
            apiV1ServiceBuilder
                    .annotatedService(new MetadataApiService(executor, mds, authCfg.loginNameNormalizer()))
                    .annotatedService(new TokenService(executor, mds));

            // authentication services:
            Optional.ofNullable(authProvider.loginApiService())
                    .ifPresent(login -> LOGIN_API_ROUTES.forEach(mapping -> sb.service(mapping, login)));

            // Provide logout API by default.
            final HttpService logout =
                    Optional.ofNullable(authProvider.logoutApiService())
                            .orElseGet(() -> new DefaultLogoutService(executor));
            for (Route route : LOGOUT_API_ROUTES) {
                sb.service(route, decorator.apply(logout));
            }

            authProvider.moreServices().forEach(sb::service);
        }

        sb.annotatedService()
          .decorator(decorator)
          .decorator(DecodingService.newDecorator())
          .build(new GitHttpService(projectApiManager));

        if (cfg.isWebAppEnabled()) {
            sb.contextPath(API_V0_PATH_PREFIX)
              .annotatedService(new UserService(executor))
              .annotatedService(new RepositoryService(projectApiManager, executor));

            if (authProvider != null) {
                // Will redirect to /web/auth/login by default.
                sb.service(LOGIN_PATH, authProvider.webLoginService());
                // Will redirect to /web/auth/logout by default.
                sb.service(LOGOUT_PATH, authProvider.webLogoutService());
            }

            // If the index.html is just returned, Next.js will handle the all remaining process such as
            // fetching resources and routes to the target pages.
            sb.serviceUnder("/app", HttpFile.of(CentralDogma.class.getClassLoader(),
                                                "com/linecorp/centraldogma/webapp/index.html")
                                            .asService());
            // Serve all web resources except for '/app'.
            sb.route()
              .pathPrefix("/")
              .exclude("prefix:/app")
              .exclude("prefix:/api")
              .build(FileService.builder(CentralDogma.class.getClassLoader(),
                                         "com/linecorp/centraldogma/webapp")
                                .cacheControl(ServerCacheControl.REVALIDATED)
                                .autoDecompress(true)
                                .serveCompressedFiles(true)
                                .fallbackFileExtensions("html")
                                .build());

            sb.serviceUnder("/legacy-web",
                            FileService.builder(CentralDogma.class.getClassLoader(), "webapp")
                                       .cacheControl(ServerCacheControl.REVALIDATED)
                                       .autoDecompress(true)
                                       .serveCompressedFiles(true)
                                       .build());
        }

        sb.errorHandler(new HttpApiExceptionHandler());
    }

    private static void configCors(ServerBuilder sb, @Nullable CorsConfig corsConfig) {
        if (corsConfig == null) {
            return;
        }

        sb.decorator(CorsService.builder(corsConfig.allowedOrigins())
                                .allowRequestMethods(HttpMethod.knownMethods())
                                .allowAllRequestHeaders(true)
                                .allowCredentials()
                                .maxAge(corsConfig.maxAgeSeconds())
                                .newDecorator());
    }

    private static void configManagement(ServerBuilder sb, @Nullable ManagementConfig managementConfig) {
        if (managementConfig == null) {
            return;
        }

        // curl -L https://
:/internal/management/jvm/threaddump // curl -L https://
:/internal/management/jvm/heapdump -o heapdump.hprof final int port = managementConfig.port(); if (port == 0) { logger.info("'management.port' is 0, using the same ports as 'ports'."); sb.route() .pathPrefix(managementConfig.path()) .defaultServiceName("management") .build(ManagementService.of()); } else { final SessionProtocol managementProtocol = managementConfig.protocol(); final String address = managementConfig.address(); if (address == null) { sb.port(new ServerPort(port, managementProtocol)); } else { sb.port(new ServerPort(new InetSocketAddress(address, port), managementProtocol)); } sb.virtualHost(port) .route() .pathPrefix(managementConfig.path()) .defaultServiceName("management") .build(ManagementService.of()); } } private static Function contentEncodingDecorator() { return delegate -> EncodingService .builder() .encodableContentTypes(contentType -> { if ("application".equals(contentType.type())) { final String subtype = contentType.subtype(); switch (subtype) { case "json": case "xml": case "x-thrift": case "x-git-upload-pack-advertisement": case "x-git-upload-pack-result": return true; default: return subtype.endsWith("+json") || subtype.endsWith("+xml") || subtype.startsWith("vnd.apache.thrift."); } } return false; }) .build(delegate); } private void configureMetrics(ServerBuilder sb, MeterRegistry registry) { sb.meterRegistry(registry); // expose the prometheus endpoint if the registry is either a PrometheusMeterRegistry or // CompositeMeterRegistry if (registry instanceof PrometheusMeterRegistry) { final PrometheusMeterRegistry prometheusMeterRegistry = (PrometheusMeterRegistry) registry; sb.service(METRICS_PATH, PrometheusExpositionService.of(prometheusMeterRegistry.getPrometheusRegistry())); } else if (registry instanceof CompositeMeterRegistry) { final PrometheusMeterRegistry prometheusMeterRegistry = PrometheusMeterRegistries.newRegistry(); ((CompositeMeterRegistry) registry).add(prometheusMeterRegistry); sb.service(METRICS_PATH, PrometheusExpositionService.of(prometheusMeterRegistry.getPrometheusRegistry())); meterRegistryToBeClosed = prometheusMeterRegistry; } else { logger.info("Not exposing a prometheus endpoint for the type: {}", registry.getClass()); } sb.decorator(MetricCollectingService.newDecorator(MeterIdPrefixFunction.ofDefault("api"))); // Bind system metrics. new FileDescriptorMetrics().bindTo(registry); new ProcessorMetrics().bindTo(registry); new ClassLoaderMetrics().bindTo(registry); new UptimeMetrics().bindTo(registry); new DiskSpaceMetrics(cfg.dataDir()).bindTo(registry); new JvmGcMetrics().bindTo(registry); new JvmMemoryMetrics().bindTo(registry); new JvmThreadMetrics().bindTo(registry); // Bind global thread pool metrics. ExecutorServiceMetrics.monitor(registry, ForkJoinPool.commonPool(), "commonPool"); } private void doStop() { if (server == null) { return; } final Server server = this.server; final CommandExecutor executor = this.executor; final ProjectManager pm = this.pm; final ExecutorService repositoryWorker = this.repositoryWorker; final ExecutorService purgeWorker = this.purgeWorker; final SessionManager sessionManager = this.sessionManager; this.server = null; this.executor = null; this.pm = null; this.repositoryWorker = null; this.sessionManager = null; projectInitializer = null; if (meterRegistryToBeClosed != null) { assert meterRegistry instanceof CompositeMeterRegistry; ((CompositeMeterRegistry) meterRegistry).remove(meterRegistryToBeClosed); meterRegistryToBeClosed.close(); meterRegistryToBeClosed = null; } logger.info("Stopping the Central Dogma .."); if (!doStop(server, executor, pm, repositoryWorker, purgeWorker, sessionManager)) { logger.warn("Stopped the Central Dogma with failure."); } else { logger.info("Stopped the Central Dogma successfully."); } } private static boolean doStop( @Nullable Server server, @Nullable CommandExecutor executor, @Nullable ProjectManager pm, @Nullable ExecutorService repositoryWorker, @Nullable ExecutorService purgeWorker, @Nullable SessionManager sessionManager) { boolean success = true; try { if (sessionManager != null) { logger.info("Stopping the session manager .."); sessionManager.close(); logger.info("Stopped the session manager."); } } catch (Throwable t) { success = false; logger.warn("Failed to stop the session manager:", t); } try { if (pm != null) { logger.info("Stopping the project manager .."); pm.close(ShuttingDownException::new); logger.info("Stopped the project manager."); } } catch (Throwable t) { success = false; logger.warn("Failed to stop the project manager:", t); } try { if (executor != null) { logger.info("Stopping the command executor .."); executor.stop(); logger.info("Stopped the command executor."); } } catch (Throwable t) { success = false; logger.warn("Failed to stop the command executor:", t); } final BiFunction stopWorker = (worker, name) -> { try { if (worker != null && !worker.isTerminated()) { logger.info("Stopping the {} worker ..", name); boolean interruptLater = false; while (!worker.isTerminated()) { worker.shutdownNow(); try { worker.awaitTermination(1, TimeUnit.SECONDS); } catch (InterruptedException e) { // Interrupt later. interruptLater = true; } } logger.info("Stopped the {} worker.", name); if (interruptLater) { Thread.currentThread().interrupt(); } } return true; } catch (Throwable t) { logger.warn("Failed to stop the " + name + " worker:", t); return false; } }; if (!stopWorker.apply(repositoryWorker, "repository")) { success = false; } if (!stopWorker.apply(purgeWorker, "purge")) { success = false; } try { if (server != null) { logger.info("Stopping the RPC server .."); server.stop().join(); logger.info("Stopped the RPC server."); } } catch (Throwable t) { success = false; logger.warn("Failed to stop the RPC server:", t); } return success; } private final class CentralDogmaStartStop extends StartStopSupport { @Nullable private final PluginGroup pluginsForAllReplicas; CentralDogmaStartStop(@Nullable PluginGroup pluginsForAllReplicas) { super(GlobalEventExecutor.INSTANCE); this.pluginsForAllReplicas = pluginsForAllReplicas; } @Override protected CompletionStage doStart(@Nullable Void unused) throws Exception { return execute("startup", () -> { try { CentralDogma.this.doStart(); if (pluginsForAllReplicas != null) { final ProjectManager pm = CentralDogma.this.pm; final CommandExecutor executor = CentralDogma.this.executor; final MeterRegistry meterRegistry = CentralDogma.this.meterRegistry; if (pm != null && executor != null && meterRegistry != null) { pluginsForAllReplicas.start(cfg, pm, executor, meterRegistry, purgeWorker, projectInitializer).join(); } } } catch (Exception e) { Exceptions.throwUnsafely(e); } }); } @Override protected CompletionStage doStop(@Nullable Void unused) throws Exception { return execute("shutdown", () -> { if (pluginsForAllReplicas != null) { final ProjectManager pm = CentralDogma.this.pm; final CommandExecutor executor = CentralDogma.this.executor; final MeterRegistry meterRegistry = CentralDogma.this.meterRegistry; if (pm != null && executor != null && meterRegistry != null) { pluginsForAllReplicas.stop(cfg, pm, executor, meterRegistry, purgeWorker, projectInitializer).join(); } } CentralDogma.this.doStop(); }); } private CompletionStage execute(String mode, Runnable task) { final CompletableFuture future = new CompletableFuture<>(); final Thread thread = new Thread(() -> { try { task.run(); future.complete(null); } catch (Throwable cause) { future.completeExceptionally(cause); } }, "dogma-" + mode + "-0x" + Long.toHexString(CentralDogma.this.hashCode() & 0xFFFFFFFFL)); thread.start(); return future; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy