Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ManagementService #1034

Merged
merged 2 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
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;
Expand Down Expand Up @@ -106,6 +107,7 @@
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;
Expand Down Expand Up @@ -568,7 +570,11 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
sb.verboseResponses(true);
cfg.ports().forEach(sb::port);

if (cfg.ports().stream().anyMatch(ServerPort::hasTls)) {
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) {
Expand Down Expand Up @@ -610,6 +616,7 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
sb.service(HEALTH_CHECK_PATH, HealthCheckService.builder()
.checkers(serverHealth)
.build());
configManagement(sb, config().managementConfig());

sb.serviceUnder("/docs/",
DocService.builder()
Expand Down Expand Up @@ -914,6 +921,36 @@ private static void configCors(ServerBuilder sb, @Nullable CorsConfig corsConfig
.newDecorator());
}

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

// curl -L https://<address>:<port>/internal/management/jvm/threaddump
// curl -L https://<address>:<port>/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<? super HttpService, EncodingService> contentEncodingDecorator() {
return delegate -> EncodingService
.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ public final class CentralDogmaBuilder {
private CorsConfig corsConfig;

private final List<PluginConfig> pluginConfigs = new ArrayList<>();
@Nullable
private ManagementConfig managementConfig;

/**
* Creates a new builder with the specified data directory.
Expand Down Expand Up @@ -529,6 +531,15 @@ public CentralDogmaBuilder pluginConfigs(PluginConfig... pluginConfigs) {
return this;
}

/**
* Enables a management service with the specified {@link ManagementConfig}.
*/
public CentralDogmaBuilder management(ManagementConfig managementConfig) {
requireNonNull(managementConfig, "managementConfig");
this.managementConfig = managementConfig;
return this;
}

/**
* Returns a newly-created {@link CentralDogma} server.
*/
Expand Down Expand Up @@ -562,6 +573,6 @@ private CentralDogmaConfig buildConfig() {
maxRemovedRepositoryAgeMillis, gracefulShutdownTimeout,
webAppEnabled, webAppTitle,replicationConfig,
null, accessLogFormat, authCfg, quotaConfig,
corsConfig, pluginConfigs);
corsConfig, pluginConfigs, managementConfig);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,14 @@ public static CentralDogmaConfig load(String json) throws JsonMappingException,
private final List<PluginConfig> pluginConfigs;
private final Map<Class<?>, PluginConfig> pluginConfigMap;

@Nullable
private final ManagementConfig managementConfig;

CentralDogmaConfig(
@JsonProperty(value = "dataDir", required = true) File dataDir,
@JsonProperty(value = "ports", required = true)
@JsonDeserialize(contentUsing = ServerPortDeserializer.class)
List<ServerPort> ports,
List<ServerPort> ports,
@JsonProperty("tls") @Nullable TlsConfig tls,
@JsonProperty("trustedProxyAddresses") @Nullable List<String> trustedProxyAddresses,
@JsonProperty("clientAddressSources") @Nullable List<String> clientAddressSources,
Expand All @@ -291,7 +294,8 @@ public static CentralDogmaConfig load(String json) throws JsonMappingException,
@JsonProperty("authentication") @Nullable AuthConfig authConfig,
@JsonProperty("writeQuotaPerRepository") @Nullable QuotaConfig writeQuotaPerRepository,
@JsonProperty("cors") @Nullable CorsConfig corsConfig,
@JsonProperty("pluginConfigs") @Nullable List<PluginConfig> pluginConfigs) {
@JsonProperty("pluginConfigs") @Nullable List<PluginConfig> pluginConfigs,
@JsonProperty("management") @Nullable ManagementConfig managementConfig) {

this.dataDir = requireNonNull(dataDir, "dataDir");
this.ports = ImmutableList.copyOf(requireNonNull(ports, "ports"));
Expand Down Expand Up @@ -339,6 +343,7 @@ public static CentralDogmaConfig load(String json) throws JsonMappingException,
this.pluginConfigs = firstNonNull(pluginConfigs, ImmutableList.of());
pluginConfigMap = this.pluginConfigs.stream().collect(
toImmutableMap(PluginConfig::getClass, Function.identity()));
this.managementConfig = managementConfig;
}

/**
Expand Down Expand Up @@ -568,6 +573,15 @@ public Map<Class<?>, PluginConfig> pluginConfigMap() {
return pluginConfigMap;
}

/**
* Returns the {@link ManagementConfig}.
*/
@Nullable
@JsonProperty("management")
public ManagementConfig managementConfig() {
return managementConfig;
}

@Override
public String toString() {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2024 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 java.util.Objects;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.MoreObjects;

import com.linecorp.armeria.common.SessionProtocol;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.server.management.ManagementService;

/**
* A configuration for the {@link ManagementService}.
*/
public final class ManagementConfig {
private static final String DEFAULT_PROTOCOL = "http";
private static final String DEFAULT_PATH = "/internal/management";

private final SessionProtocol protocol;
private final @Nullable String address;
private final int port;
private final String path;

/**
* Creates a new instance.
*/
@JsonCreator
public ManagementConfig(@JsonProperty("protocol") @Nullable String protocol,
@JsonProperty("address") @Nullable String address,
@JsonProperty("port") int port,
@JsonProperty("path") @Nullable String path) {
this(SessionProtocol.of(firstNonNull(protocol, DEFAULT_PROTOCOL)),
address, port, path);
}

/**
* Creates a new instance.
*/
public ManagementConfig(@Nullable SessionProtocol protocol,
@Nullable String address,
int port,
@Nullable String path) {
protocol = firstNonNull(protocol, SessionProtocol.HTTP);
checkArgument(protocol != SessionProtocol.PROXY, "protocol: %s (expected: one of %s)",
protocol, SessionProtocol.httpAndHttpsValues());
this.protocol = protocol;
this.address = address;
checkArgument(port >= 0 && port <= 65535, "management.port: %s (expected: 0-65535)", port);
this.port = port;
this.path = firstNonNull(path, DEFAULT_PATH);
}

/**
* Returns the protocol of the management service.
*/
@JsonProperty("protocol")
public SessionProtocol protocol() {
return protocol;
}

/**
* Returns the address of the management service.
*/
@JsonProperty("address")
public @Nullable String address() {
return address;
}

/**
* Returns the port of the management service.
*/
@JsonProperty("port")
public int port() {
return port;
}

/**
* Returns the path of the management service.
*/
@JsonProperty("path")
public String path() {
return path;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ManagementConfig)) {
return false;
}
final ManagementConfig that = (ManagementConfig) o;
return port == that.port &&
protocol == that.protocol &&
Objects.equals(address, that.address) &&
path.equals(that.path);
}

@Override
public int hashCode() {
return Objects.hash(protocol, address, port, path);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("protocol", protocol)
.add("address", address)
.add("port", port)
.add("path", path)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2024 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 org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.core.JsonProcessingException;

import com.linecorp.armeria.client.BlockingWebClient;
import com.linecorp.armeria.client.ClientFactory;
import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.common.AggregatedHttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.SessionProtocol;
import com.linecorp.armeria.internal.common.util.PortUtil;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension;

class ManagementServiceTest {

private static int tlsPort;

@RegisterExtension
static final CentralDogmaExtension noManagement = new CentralDogmaExtension();

@RegisterExtension
static final CentralDogmaExtension management = new CentralDogmaExtension() {
@Override
protected void configure(CentralDogmaBuilder builder) {
builder.management(new ManagementConfig((String) null, null, 0, null));
}
};

@RegisterExtension
static final CentralDogmaExtension managementWithFullOptions = new CentralDogmaExtension() {
@Override
protected void configure(CentralDogmaBuilder builder) {
tlsPort = PortUtil.unusedTcpPort();
builder.management(
new ManagementConfig(SessionProtocol.HTTPS, "127.0.0.1", tlsPort, "/custom/management"));
}
};

@Test
void disableManagementServiceByDefault() {
final BlockingWebClient client = noManagement.blockingHttpClient();
assertThat(client.get("/internal/management").status()).isEqualTo(HttpStatus.NOT_FOUND);
}

@Test
void managementServiceWithDefaultOption() {
final BlockingWebClient client = management.blockingHttpClient();
final AggregatedHttpResponse response = client.get("/internal/management/jvm/threaddump");
assertThat(response.status()).isEqualTo(HttpStatus.OK);
assertThat(response.contentUtf8()).contains("repository-worker-");
}

@Test
void managementServiceWithFullOptions() {
final BlockingWebClient client =
WebClient.builder("https://127.0.0.1:" + tlsPort)
.factory(ClientFactory.insecure())
.build()
.blocking();
final AggregatedHttpResponse response = client.get("/custom/management/jvm/threaddump");
assertThat(response.status()).isEqualTo(HttpStatus.OK);
assertThat(response.contentUtf8()).contains("repository-worker-");
}

@Test
void testJsonDeserialization() throws JsonProcessingException {
final String json =
'{' +
"\"protocol\":\"https\"," +
"\"address\":\"127.0.0.1\"," +
"\"port\":8443," +
"\"path\":\"/custom/management\"" +
'}';
final ManagementConfig managementConfig = Jackson.readValue(json, ManagementConfig.class);

assertThat(managementConfig.protocol()).isEqualTo(SessionProtocol.HTTPS);
assertThat(managementConfig.port()).isEqualTo(8443);
assertThat(managementConfig.address()).isEqualTo("127.0.0.1");
assertThat(managementConfig.path()).isEqualTo("/custom/management");
}
}
Loading
Loading