From 0b07aadf57f6ba8fd0dfd17c94999ccfe59315c1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EA=B5=AC=EA=B2=BD=EC=9D=BC?=
<62415185+flex-gyeongil@users.noreply.github.com>
Date: Mon, 23 Sep 2024 18:15:24 +0900
Subject: [PATCH] Spring Boot Centraldogma Loader
---
build.gradle | 63 ++---
cloud-native/boot-loader/build.gradle | 36 +++
cloud-native/boot-loader/gradle.properties | 3 +
.../armeria/server/saml/SamlEndpoint.java | 156 ++++++++++++
.../armeria/server/saml/package-info.java | 25 ++
.../centraldogma/config/AuthConfig.kt | 135 ++++++++++
.../config/GracefulShutdownTimeout.kt | 42 ++++
.../config/MirroringServicePluginConfig.kt | 58 +++++
.../centraldogma/config/QuotaConfig.kt | 70 ++++++
.../centraldogma/config/ServerPort.kt | 27 ++
.../linecorp/centraldogma/config/TlsConfig.kt | 32 +++
.../CustomConfigBindHandlerConfiguration.kt | 95 +++++++
.../server/auth/saml/AuthConfig.kt | 233 ++++++++++++++++++
.../auth/saml/SamlAuthProviderFactory.kt | 81 ++++++
.../spring/loader/CentralDogmaProperties.kt | 190 ++++++++++++++
.../spring/loader/CentralDogmaSpringLoader.kt | 108 ++++++++
.../src/main/resources/application.yaml | 28 +++
.../loader/ConfigurationPropertiesTest.kt | 133 ++++++++++
.../src/test/resources/full-config.yaml | 104 ++++++++
common/build.gradle | 1 +
.../centraldogma/internal/Jackson.java | 4 +-
dependencies.toml | 19 ++
.../it/ReplicationWriteQuotaTest.java | 13 +-
.../it/TestAllReplicasPlugin.java | 8 +-
.../centraldogma/it/WriteQuotaTestBase.java | 9 +-
.../it/zoneleader/ZoneLeaderPluginTest.java | 10 +-
.../auth/saml/SamlAuthProviderFactory.java | 8 +-
.../server/auth/saml/SamlAuthSsoHandler.java | 6 +-
.../server/auth/saml/SamlSession.java | 45 ++++
.../saml/SessionGroupTokenAuthorizer.java | 93 +++++++
.../AuthenticationDeserializationTest.java | 7 +-
.../server/auth/shiro/ShiroAuthProvider.java | 6 +-
.../auth/shiro/ShiroAuthProviderFactory.java | 10 +-
.../server/AbstractCentralDogmaConfig.java | 110 +++++++++
.../centraldogma/server/CentralDogma.java | 113 +++++++--
.../server/CentralDogmaBuilder.java | 20 +-
.../server/CentralDogmaConfig.java | 196 ++++-----------
.../server/CentralDogmaConfigSpec.java | 210 ++++++++++++++++
.../centraldogma/server/CorsConfig.java | 17 +-
.../centraldogma/server/CorsConfigSpec.java | 41 +++
.../server/GracefulShutdownTimeout.java | 12 +-
.../server/GracefulShutdownTimeoutSpec.java | 32 +++
.../centraldogma/server/ManagementConfig.java | 20 +-
.../server/ManagementConfigSpec.java | 50 ++++
.../centraldogma/server/PluginGroup.java | 35 +--
.../centraldogma/server/QuotaConfig.java | 19 +-
.../centraldogma/server/QuotaConfigSpec.java | 39 +++
.../centraldogma/server/TlsConfig.java | 16 +-
.../centraldogma/server/TlsConfigSpec.java | 46 ++++
.../centraldogma/server/ZoneConfig.java | 10 +-
.../centraldogma/server/ZoneConfigSpec.java | 34 +++
.../server/ZooKeeperReplicationConfig.java | 129 ++--------
.../ZooKeeperReplicationConfigSpec.java | 162 ++++++++++++
.../server/ZooKeeperServerConfig.java | 46 ++--
.../server/ZooKeeperServerConfigSpec.java | 68 +++++
.../centraldogma/server/auth/AuthConfig.java | 74 +-----
.../server/auth/AuthConfigSpec.java | 109 ++++++++
.../server/auth/AuthProviderParameters.java | 14 +-
.../centraldogma/server/auth/Session.java | 16 +-
.../server/command/CommandExecutor.java | 8 +-
.../command/ForwardingCommandExecutor.java | 6 +-
.../command/StandaloneCommandExecutor.java | 8 +-
.../admin/auth/FileBasedSessionManager.java | 6 +-
.../internal/api/MetadataApiService.java | 5 +-
.../internal/api/MirroringServiceV1.java | 12 +-
.../mirror/DefaultMirroringServicePlugin.java | 20 +-
.../server/internal/mirror/MirrorRunner.java | 4 +-
.../mirror/MirrorSchedulingService.java | 6 +-
.../replication/ZooKeeperCommandExecutor.java | 39 +--
.../storage/PurgeSchedulingServicePlugin.java | 17 +-
.../repository/DefaultMetaRepository.java | 6 +-
.../mirror/MirroringServicePluginConfig.java | 19 +-
.../MirroringServicePluginConfigSpec.java | 43 ++++
.../server/plugin/AllReplicasPlugin.java | 6 +-
.../server/plugin/NoopPluginConfig.java | 32 +++
.../centraldogma/server/plugin/Plugin.java | 12 +-
.../server/plugin/PluginContext.java | 9 +-
.../server/plugin/PluginInitContext.java | 6 +-
.../storage/repository/MetaRepository.java | 4 +-
.../server/CentralDogmaConfigTest.java | 18 +-
.../server/ConfigDeserializationTest.java | 6 +-
.../centraldogma/server/PluginGroupTest.java | 25 +-
.../ZooKeeperReplicationConfigTest.java | 8 +-
.../auth/ReplicatedLoginAndLogoutTest.java | 5 +-
.../CommandExecutorStatusManagerTest.java | 4 +-
.../MirrorSchedulingServicePluginTest.java | 6 +-
.../internal/replication/ClusterBuilder.java | 5 +-
.../server/internal/replication/Replica.java | 12 +-
.../StartStopWithoutInitialQuorumTest.java | 5 +-
.../ZooKeeperCommandExecutorTest.java | 8 +-
.../server/plugin/AbstractNoopPlugin.java | 7 +-
.../plugin/NoopPluginForAllReplicas.java | 12 +-
.../server/plugin/NoopPluginForLeader.java | 12 +-
settings.gradle | 5 +-
site/src/sphinx/auth.rst | 2 +-
.../CentralDogmaReplicationExtension.java | 12 +-
.../internal/CentralDogmaRuleDelegate.java | 9 +-
.../xds/internal/ControlPlanePlugin.java | 5 +-
.../XdsKubernetesEndpointFetchingPlugin.java | 10 +-
.../ControlPlanePluginConfigTest.java | 2 +-
.../internal/CreatingInternalGroupPlugin.java | 6 +-
101 files changed, 3223 insertions(+), 725 deletions(-)
create mode 100644 cloud-native/boot-loader/build.gradle
create mode 100644 cloud-native/boot-loader/gradle.properties
create mode 100644 cloud-native/boot-loader/src/main/java/com/linecorp/armeria/server/saml/SamlEndpoint.java
create mode 100644 cloud-native/boot-loader/src/main/java/com/linecorp/armeria/server/saml/package-info.java
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/AuthConfig.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/GracefulShutdownTimeout.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/MirroringServicePluginConfig.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/QuotaConfig.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/ServerPort.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/TlsConfig.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/CustomConfigBindHandlerConfiguration.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/auth/saml/AuthConfig.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/auth/saml/SamlAuthProviderFactory.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/spring/loader/CentralDogmaProperties.kt
create mode 100644 cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/spring/loader/CentralDogmaSpringLoader.kt
create mode 100644 cloud-native/boot-loader/src/main/resources/application.yaml
create mode 100644 cloud-native/boot-loader/src/test/kotlin/com/linecorp/centraldogma/spring/loader/ConfigurationPropertiesTest.kt
create mode 100644 cloud-native/boot-loader/src/test/resources/full-config.yaml
create mode 100644 server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlSession.java
create mode 100644 server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SessionGroupTokenAuthorizer.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/AbstractCentralDogmaConfig.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/CorsConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/GracefulShutdownTimeoutSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/ManagementConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/QuotaConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/TlsConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/ZoneConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperServerConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/mirror/MirroringServicePluginConfigSpec.java
create mode 100644 server/src/main/java/com/linecorp/centraldogma/server/plugin/NoopPluginConfig.java
diff --git a/build.gradle b/build.gradle
index e8e111470..7ee1f6c56 100644
--- a/build.gradle
+++ b/build.gradle
@@ -13,6 +13,9 @@ buildscript {
plugins {
alias libs.plugins.nexus.publish
alias libs.plugins.osdetector apply false
+ alias libs.plugins.kotlin apply false
+ alias libs.plugins.ktlint apply false
+ alias libs.plugins.jib apply false
}
allprojects {
@@ -78,28 +81,11 @@ configure(projectsWithFlags('java')) {
compileOnly libs.jetty.alpn.api
// Logging
- if (project.name.startsWith("java-spring-boot3")) {
- implementation libs.slf4j2.api
- testImplementation libs.slf4j2.jul.to.slf4j
- testImplementation libs.slf4j2.jcl.over.slf4j
- testImplementation libs.slf4j2.log4j.over.slf4j
- testRuntimeOnly libs.logback15
- configurations.configureEach {
- resolutionStrategy {
- force libs.slf4j2.api.get()
- force libs.slf4j2.jul.to.slf4j.get()
- force libs.slf4j2.jcl.over.slf4j.get()
- force libs.slf4j2.log4j.over.slf4j.get()
- force libs.logback15.get()
- }
- }
- } else {
- implementation libs.slf4j1.api
- testImplementation libs.slf4j1.jul.to.slf4j
- testImplementation libs.slf4j1.jcl.over.slf4j
- testImplementation libs.slf4j1.log4j.over.slf4j
- testRuntimeOnly libs.logback12
- }
+ implementation libs.slf4j1.api
+ testImplementation libs.slf4j1.jul.to.slf4j
+ testImplementation libs.slf4j1.jcl.over.slf4j
+ testImplementation libs.slf4j1.log4j.over.slf4j
+ testRuntimeOnly libs.logback12
// Test-time dependencies
testImplementation libs.json.unit.fluent
@@ -115,12 +101,9 @@ configure(projectsWithFlags('java')) {
testRuntimeOnly libs.junit5.vintage.engine
}
- // Target Java 8 except for spring-boot3
- if (!project.name.startsWith("java-spring-boot3")) {
- tasks.withType(JavaCompile) {
- if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
- options.release = 8
- }
+ tasks.withType(JavaCompile) {
+ if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
+ options.release = 8
}
}
@@ -218,6 +201,30 @@ class TestsReportTask extends DefaultTask {
}
}
+configure(projectsWithFlags('spring-boot3')) {
+
+ tasks.withType(JavaCompile) {
+ options.release = 17
+ }
+
+ dependencies {
+ implementation libs.slf4j2.api
+ testImplementation libs.slf4j2.jul.to.slf4j
+ testImplementation libs.slf4j2.jcl.over.slf4j
+ testImplementation libs.slf4j2.log4j.over.slf4j
+ testRuntimeOnly libs.logback15
+ configurations.configureEach {
+ resolutionStrategy {
+ force libs.slf4j2.api.get()
+ force libs.slf4j2.jul.to.slf4j.get()
+ force libs.slf4j2.jcl.over.slf4j.get()
+ force libs.slf4j2.log4j.over.slf4j.get()
+ force libs.logback15.get()
+ }
+ }
+ }
+}
+
// Configure the Javadoc tasks of all projects.
allprojects {
tasks.withType(Javadoc) {
diff --git a/cloud-native/boot-loader/build.gradle b/cloud-native/boot-loader/build.gradle
new file mode 100644
index 000000000..f080a0a89
--- /dev/null
+++ b/cloud-native/boot-loader/build.gradle
@@ -0,0 +1,36 @@
+plugins {
+ alias libs.plugins.spring.boot
+ alias libs.plugins.kotlin.spring
+ alias libs.plugins.jib
+}
+
+dependencies {
+ implementation libs.spring.boot3.starter.web
+ implementation libs.spring.boot3.starter.actuator
+ implementation libs.jackson.kotlin
+ implementation project(":server")
+ implementation project(":server-auth:saml")
+ implementation project(':server-mirror-git')
+
+ implementation(libs.armeria.saml) {
+ exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
+ }
+
+ implementation 'org.opensaml:opensaml-security-impl:3.4.6'
+ implementation 'org.opensaml:opensaml-saml-impl:3.4.6'
+
+ testImplementation libs.spring.boot3.starter.test
+
+ testImplementation("com.willowtreeapps.assertk:assertk:0.28.1")
+}
+
+jib {
+ from {
+ image = 'eclipse-temurin:21-jre-jammy'
+ }
+
+ container {
+ creationTime = 'USE_CURRENT_TIMESTAMP'
+ ports = ['8080', '36462']
+ }
+}
diff --git a/cloud-native/boot-loader/gradle.properties b/cloud-native/boot-loader/gradle.properties
new file mode 100644
index 000000000..cae212e42
--- /dev/null
+++ b/cloud-native/boot-loader/gradle.properties
@@ -0,0 +1,3 @@
+javaSourceCompatibility=17
+javaTargetCompatibility=17
+minimumJavaVersion=17
diff --git a/cloud-native/boot-loader/src/main/java/com/linecorp/armeria/server/saml/SamlEndpoint.java b/cloud-native/boot-loader/src/main/java/com/linecorp/armeria/server/saml/SamlEndpoint.java
new file mode 100644
index 000000000..b8992a21e
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/java/com/linecorp/armeria/server/saml/SamlEndpoint.java
@@ -0,0 +1,156 @@
+/*
+ * 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.armeria.server.saml;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.linecorp.armeria.server.saml.SamlPortConfig.validatePort;
+import static java.util.Objects.requireNonNull;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.springframework.web.util.UriComponentsBuilder;
+
+import com.google.common.base.MoreObjects;
+
+import com.linecorp.armeria.common.annotation.Nullable;
+import com.linecorp.armeria.common.util.Exceptions;
+
+/**
+ * A SAML service URL and its binding protocol.
+ */
+public final class SamlEndpoint {
+ /**
+ * Creates a {@link SamlEndpoint} of the specified {@code uri} and the HTTP Redirect binding protocol.
+ */
+ public static SamlEndpoint ofHttpRedirect(String uri) {
+ requireNonNull(uri, "uri");
+ try {
+ return ofHttpRedirect(new URI(uri));
+ } catch (URISyntaxException e) {
+ return Exceptions.throwUnsafely(e);
+ }
+ }
+
+ /**
+ * Creates a {@link SamlEndpoint} of the specified {@code uri} and the HTTP Redirect binding protocol.
+ */
+ public static SamlEndpoint ofHttpRedirect(URI uri) {
+ requireNonNull(uri, "uri");
+ return new SamlEndpoint(uri, SamlBindingProtocol.HTTP_REDIRECT);
+ }
+
+ /**
+ * Creates a {@link SamlEndpoint} of the specified {@code uri} and the HTTP POST binding protocol.
+ */
+ public static SamlEndpoint ofHttpPost(String uri) {
+ requireNonNull(uri, "uri");
+ try {
+ return ofHttpPost(new URI(uri));
+ } catch (URISyntaxException e) {
+ return Exceptions.throwUnsafely(e);
+ }
+ }
+
+ /**
+ * Creates a {@link SamlEndpoint} of the specified {@code uri} and the HTTP POST binding protocol.
+ */
+ public static SamlEndpoint ofHttpPost(URI uri) {
+ requireNonNull(uri, "uri");
+ return new SamlEndpoint(uri, SamlBindingProtocol.HTTP_POST);
+ }
+
+ private final URI uri;
+ private final SamlBindingProtocol bindingProtocol;
+ private final String uriAsString;
+
+ private SamlEndpoint(URI uri, SamlBindingProtocol bindingProtocol) {
+ this.uri = uri;
+ this.bindingProtocol = bindingProtocol;
+ uriAsString = uri.toString();
+ }
+
+ /**
+ * Returns a {@link URI} of this endpoint.
+ */
+ public URI uri() {
+ return uri;
+ }
+
+ /**
+ * Returns a {@link URI} of this endpoint as a string.
+ */
+ public String toUriString() {
+ return uriAsString;
+ }
+
+ /**
+ * Returns a {@link URI} of this endpoint as a string. The omitted values in the {@link URI} will be
+ * replaced with the specified default values, such as {@code defaultScheme}, {@code defaultHostname}
+ * and {@code defaultPort}.
+ */
+ String toUriString(String defaultScheme, String defaultHostname, int defaultPort) {
+ requireNonNull(defaultScheme, "defaultScheme");
+ requireNonNull(defaultHostname, "defaultHostname");
+ validatePort(defaultPort);
+
+ final String scheme = firstNonNull(uri.getScheme(), defaultScheme);
+ final UriComponentsBuilder builder = UriComponentsBuilder.newInstance()
+ .scheme(scheme)
+ .host(firstNonNull(uri.getHost(), defaultHostname));
+
+ // FIXME Should be applied to armeria upstream!!
+ final int port = uri.getPort() > 0 ? uri.getPort() : defaultPort;
+ if (!(port == 80 && "http".equals(scheme) || port == 443 && "https".equals(scheme))) {
+ builder.port(uri.getPort() > 0 ? uri.getPort() : defaultPort);
+ }
+ return builder
+ .path(uri.getPath())
+ .toUriString();
+ }
+
+ /**
+ * Returns a {@link SamlBindingProtocol} of this endpoint.
+ */
+ public SamlBindingProtocol bindingProtocol() {
+ return bindingProtocol;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SamlEndpoint)) {
+ return false;
+ }
+ final SamlEndpoint that = (SamlEndpoint) o;
+ return uri().equals(that.uri()) && bindingProtocol() == that.bindingProtocol();
+ }
+
+ @Override
+ public int hashCode() {
+ return uri().hashCode() * 31 + bindingProtocol().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("uri", uri)
+ .add("bindingProtocol", bindingProtocol)
+ .toString();
+ }
+}
diff --git a/cloud-native/boot-loader/src/main/java/com/linecorp/armeria/server/saml/package-info.java b/cloud-native/boot-loader/src/main/java/com/linecorp/armeria/server/saml/package-info.java
new file mode 100644
index 000000000..251d88008
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/java/com/linecorp/armeria/server/saml/package-info.java
@@ -0,0 +1,25 @@
+
+/*
+ * Copyright 2018 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.
+ */
+
+/**
+ * SAML-based single sign-on
+ * service.
+ */
+@NonNullByDefault
+package com.linecorp.armeria.server.saml;
+
+import com.linecorp.armeria.common.annotation.NonNullByDefault;
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/AuthConfig.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/AuthConfig.kt
new file mode 100644
index 000000000..272769660
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/AuthConfig.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.config
+
+import com.google.common.base.Ascii
+import com.linecorp.centraldogma.internal.Jackson
+import com.linecorp.centraldogma.server.ReplicationConfig
+import com.linecorp.centraldogma.server.ReplicationMethod
+import com.linecorp.centraldogma.server.ZooKeeperReplicationConfigSpec
+import com.linecorp.centraldogma.server.ZooKeeperServerConfigSpec
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec
+import com.linecorp.centraldogma.server.auth.AuthProviderFactory
+import org.springframework.validation.annotation.Validated
+import java.util.function.Function
+
+open class ReplicationConfigImpl(
+ private val method: ReplicationMethod,
+) : ReplicationConfig {
+ override fun method(): ReplicationMethod = method
+}
+
+data class NoneReplicationConfig(private val method: ReplicationMethod) : ReplicationConfigImpl(method)
+
+@Validated
+data class ZooKeeperServerConfig(
+ private val host: String,
+ private val quorumPort: Int,
+ private val electionPort: Int,
+ private val clientPort: Int,
+ private val groupId: Int?,
+ private val weight: Int = 1,
+) : ZooKeeperServerConfigSpec {
+ init {
+ check(weight >= 0) { "weight: $weight (expected: >= 0)" }
+ }
+
+ override fun host(): String = host
+
+ override fun quorumPort(): Int = ZooKeeperServerConfigSpec.validatePort(quorumPort, "quorumPort")
+
+ override fun electionPort(): Int = ZooKeeperServerConfigSpec.validatePort(electionPort, "electionPort")
+
+ override fun clientPort(): Int = clientPort
+
+ override fun groupId(): Int? = groupId
+
+ override fun weight(): Int = weight
+}
+
+data class ZooKeeperReplicationConfig(
+ private val method: ReplicationMethod,
+ private val servers: Map,
+ private val secret: String,
+ private val additionalProperties: Map = emptyMap(),
+ private val timeoutMillis: Int = ZooKeeperReplicationConfigSpec.DEFAULT_TIMEOUT_MILLIS,
+ private val numWorkers: Int = ZooKeeperReplicationConfigSpec.DEFAULT_NUM_WORKERS,
+ private val maxLogCount: Int = ZooKeeperReplicationConfigSpec.DEFAULT_MAX_LOG_COUNT,
+ private val minLogAgeMillis: Long = ZooKeeperReplicationConfigSpec.DEFAULT_MIN_LOG_AGE_MILLIS,
+) : ReplicationConfigImpl(method), ZooKeeperReplicationConfigSpec {
+ private val serverId: Int = ZooKeeperReplicationConfigSpec.findServerId(servers)
+
+ override fun serverId(): Int = serverId
+
+ override fun serverConfig(): ZooKeeperServerConfigSpec = servers[serverId]!!
+
+ override fun servers(): Map = servers
+
+ override fun secret(): String = secret
+
+ override fun additionalProperties(): Map = additionalProperties
+
+ override fun timeoutMillis(): Int = timeoutMillis
+
+ override fun numWorkers(): Int = numWorkers
+
+ override fun maxLogCount(): Int = maxLogCount
+
+ override fun minLogAgeMillis(): Long = minLogAgeMillis
+}
+
+data class AuthConfig(
+ private val factoryClassName: String,
+ private val administrators: Set = emptySet(),
+ private val caseSensitiveLoginNames: Boolean = true,
+ private val sessionCacheSpec: String = AuthConfigSpec.DEFAULT_SESSION_CACHE_SPEC,
+ private val sessionTimeoutMillis: Long = AuthConfigSpec.DEFAULT_SESSION_TIMEOUT_MILLIS,
+ private val sessionValidationSchedule: String = AuthConfigSpec.DEFAULT_SESSION_VALIDATION_SCHEDULE,
+ private val properties: Map?,
+) : AuthConfigSpec {
+ private val factory =
+ AuthConfigSpec::class.java.classLoader
+ .loadClass(factoryClassName)
+ .getDeclaredConstructor()
+ .newInstance() as AuthProviderFactory
+
+ override fun factory(): AuthProviderFactory = factory
+
+ override fun factoryClassName(): String = factoryClassName
+
+ override fun systemAdministrators(): Set = administrators
+
+ override fun caseSensitiveLoginNames(): Boolean = caseSensitiveLoginNames
+
+ override fun sessionCacheSpec(): String = sessionCacheSpec
+
+ override fun sessionTimeoutMillis(): Long = sessionTimeoutMillis
+
+ override fun sessionValidationSchedule(): String = sessionValidationSchedule
+
+ override fun properties(clazz: Class): T? =
+ properties?.let {
+ Jackson.convertValue(it, clazz)
+ }
+
+ override fun loginNameNormalizer(): Function =
+ if (caseSensitiveLoginNames) {
+ Function.identity()
+ } else {
+ Function(Ascii::toLowerCase)
+ }
+}
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/GracefulShutdownTimeout.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/GracefulShutdownTimeout.kt
new file mode 100644
index 000000000..76272594c
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/GracefulShutdownTimeout.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.config
+
+import com.google.common.base.Preconditions.checkArgument
+import com.linecorp.centraldogma.server.GracefulShutdownTimeoutSpec
+
+data class GracefulShutdownTimeout(
+ private val quietPeriodMillis: Long,
+ private val timeoutMillis: Long,
+) : GracefulShutdownTimeoutSpec {
+ init {
+ checkArgument(
+ quietPeriodMillis >= 0,
+ "quietPeriodMillis: %s (expected: >= 0)",
+ quietPeriodMillis,
+ )
+ checkArgument(
+ timeoutMillis >= 0,
+ "timeoutMillis: %s (expected: >= 0)",
+ timeoutMillis,
+ )
+ }
+
+ override fun quietPeriodMillis(): Long = quietPeriodMillis
+
+ override fun timeoutMillis(): Long = timeoutMillis
+}
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/MirroringServicePluginConfig.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/MirroringServicePluginConfig.kt
new file mode 100644
index 000000000..d69e8bdfd
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/MirroringServicePluginConfig.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.config
+
+import com.google.common.base.Preconditions.checkArgument
+import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfig
+import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfigSpec
+import com.linecorp.centraldogma.server.plugin.AbstractPluginConfig
+
+open class PluginConfigBase(
+ enabled: Boolean,
+ val type: String,
+) : AbstractPluginConfig(enabled)
+
+data class MirroringServicePluginConfig(
+ private val enabled: Boolean,
+ private val numMirroringThreads: Int = MirroringServicePluginConfigSpec.DEFAULT_NUM_MIRRORING_THREADS,
+ private val maxNumFilesPerMirror: Int = MirroringServicePluginConfigSpec.DEFAULT_MAX_NUM_FILES_PER_MIRROR,
+ private val maxNumBytesPerMirror: Long = MirroringServicePluginConfigSpec.DEFAULT_MAX_NUM_BYTES_PER_MIRROR,
+) : PluginConfigBase(enabled, MirroringServicePluginConfig::class.java.name), MirroringServicePluginConfigSpec {
+ init {
+ checkArgument(
+ numMirroringThreads > 0,
+ "numMirroringThreads: %s (expected: > 0)",
+ numMirroringThreads,
+ )
+ checkArgument(
+ maxNumFilesPerMirror > 0,
+ "maxNumFilesPerMirror: %s (expected: > 0)",
+ maxNumFilesPerMirror,
+ )
+ checkArgument(
+ maxNumBytesPerMirror > 0,
+ "maxNumBytesPerMirror: %s (expected: > 0)",
+ maxNumBytesPerMirror,
+ )
+ }
+
+ override fun numMirroringThreads(): Int = numMirroringThreads
+
+ override fun maxNumFilesPerMirror(): Int = maxNumFilesPerMirror
+
+ override fun maxNumBytesPerMirror(): Long = maxNumBytesPerMirror
+}
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/QuotaConfig.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/QuotaConfig.kt
new file mode 100644
index 000000000..21d465828
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/QuotaConfig.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.config
+
+import com.google.common.base.Preconditions.checkArgument
+import com.linecorp.armeria.common.SessionProtocol
+import com.linecorp.centraldogma.server.CorsConfigSpec
+import com.linecorp.centraldogma.server.ManagementConfigSpec
+import com.linecorp.centraldogma.server.QuotaConfigSpec
+
+data class QuotaConfig(
+ private val requestQuota: Int,
+ private val timeWindowSeconds: Int,
+) : QuotaConfigSpec {
+ override fun timeWindowSeconds(): Int = timeWindowSeconds
+
+ override fun requestQuota(): Int = requestQuota
+}
+
+data class CorsConfig(
+ private val allowedOrigins: List,
+ private val maxAgeSecond: Int,
+) : CorsConfigSpec {
+ override fun allowedOrigins(): List = allowedOrigins
+
+ override fun maxAgeSeconds(): Int = maxAgeSecond
+}
+
+data class ManagementConfig(
+ private val protocol: String = ManagementConfigSpec.DEFAULT_PROTOCOL,
+ private val address: String?,
+ private val port: Int,
+ private val path: String = ManagementConfigSpec.DEFAULT_PATH,
+) : ManagementConfigSpec {
+ private val sessionProtocol =
+ SessionProtocol.of(protocol).also {
+ checkArgument(
+ it != SessionProtocol.PROXY,
+ "protocol: %s (expected: one of %s)",
+ protocol,
+ SessionProtocol.httpAndHttpsValues(),
+ )
+ }
+
+ init {
+ checkArgument(port in 0..65535, "management.port: %s (expected: 0-65535)", port)
+ }
+
+ override fun protocol(): SessionProtocol = sessionProtocol
+
+ override fun address(): String? = address
+
+ override fun port(): Int = port
+
+ override fun path(): String = path
+}
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/ServerPort.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/ServerPort.kt
new file mode 100644
index 000000000..8d34632e7
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/ServerPort.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.config
+
+data class InetSocketAddress(
+ val host: String = "*",
+ val port: Int,
+)
+
+data class ServerPort(
+ val localAddress: InetSocketAddress,
+ val protocols: List,
+)
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/TlsConfig.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/TlsConfig.kt
new file mode 100644
index 000000000..8e66a97b4
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/config/TlsConfig.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.config
+
+import com.linecorp.centraldogma.server.TlsConfigSpec
+import java.io.InputStream
+
+data class TlsConfig(
+ private val keyCertChain: String,
+ private val key: String,
+ private val password: String?,
+) : TlsConfigSpec {
+ override fun keyCertChainInputStream(): InputStream = keyCertChain.byteInputStream()
+
+ override fun keyInputStream(): InputStream = key.byteInputStream()
+
+ override fun keyPassword(): String? = password
+}
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/CustomConfigBindHandlerConfiguration.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/CustomConfigBindHandlerConfiguration.kt
new file mode 100644
index 000000000..80ebeef21
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/CustomConfigBindHandlerConfiguration.kt
@@ -0,0 +1,95 @@
+/*
+ * 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 com.linecorp.centraldogma.config.MirroringServicePluginConfig
+import com.linecorp.centraldogma.config.NoneReplicationConfig
+import com.linecorp.centraldogma.config.PluginConfigBase
+import com.linecorp.centraldogma.config.ReplicationConfigImpl
+import com.linecorp.centraldogma.config.ZooKeeperReplicationConfig
+import org.springframework.boot.context.properties.ConfigurationPropertiesBindHandlerAdvisor
+import org.springframework.boot.context.properties.bind.AbstractBindHandler
+import org.springframework.boot.context.properties.bind.BindContext
+import org.springframework.boot.context.properties.bind.BindHandler
+import org.springframework.boot.context.properties.bind.Bindable
+import org.springframework.boot.context.properties.source.ConfigurationPropertyName
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import kotlin.reflect.KClass
+import kotlin.reflect.full.isSuperclassOf
+import kotlin.reflect.jvm.jvmName
+
+abstract class PolymorphicBindHandler(
+ parent: BindHandler,
+ private val kClass: KClass,
+) : AbstractBindHandler(parent) {
+ override fun onSuccess(
+ name: ConfigurationPropertyName?,
+ target: Bindable<*>?,
+ context: BindContext,
+ result: Any?,
+ ): Any {
+ if (result == null || !kClass.isSuperclassOf(result::class)) {
+ return super.onSuccess(name, target, context, result)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ val bindType = decideBindType(result as BASE_TYPE)
+ return context.binder.bind(name, Bindable.of(bindType.java)).get()
+ }
+
+ abstract fun decideBindType(result: BASE_TYPE): KClass
+}
+
+class ReplicationMethodBindHandlerAdvisor : ConfigurationPropertiesBindHandlerAdvisor {
+ override fun apply(bindHandler: BindHandler): BindHandler {
+ return object : PolymorphicBindHandler(
+ bindHandler,
+ ReplicationConfigImpl::class,
+ ) {
+ override fun decideBindType(result: ReplicationConfigImpl): KClass =
+ when (result.method()) {
+ ReplicationMethod.NONE -> NoneReplicationConfig::class
+ ReplicationMethod.ZOOKEEPER -> ZooKeeperReplicationConfig::class
+ }
+ }
+ }
+}
+
+class PluginConfigBindHandlerAdvisor : ConfigurationPropertiesBindHandlerAdvisor {
+ override fun apply(bindHandler: BindHandler): BindHandler {
+ return object : PolymorphicBindHandler(
+ bindHandler,
+ PluginConfigBase::class,
+ ) {
+ private val knownPluginClasses =
+ listOf(MirroringServicePluginConfig::class)
+ .associateBy { it.jvmName }
+
+ override fun decideBindType(result: PluginConfigBase): KClass {
+ return knownPluginClasses[result.type] ?: error("unknown plugin type ${result.type}")
+ }
+ }
+ }
+}
+
+@Import(
+ ReplicationMethodBindHandlerAdvisor::class,
+ PluginConfigBindHandlerAdvisor::class,
+)
+@Configuration
+class CustomConfigBindHandlerConfiguration
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/auth/saml/AuthConfig.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/auth/saml/AuthConfig.kt
new file mode 100644
index 000000000..1bb47ea0a
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/auth/saml/AuthConfig.kt
@@ -0,0 +1,233 @@
+package com.linecorp.centraldogma.server.auth.saml
+
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.google.common.base.Strings
+import com.linecorp.armeria.common.AggregatedHttpRequest
+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.server.ServiceRequestContext
+import com.linecorp.armeria.server.saml.InvalidSamlRequestException
+import com.linecorp.armeria.server.saml.SamlBindingProtocol
+import com.linecorp.armeria.server.saml.SamlEndpoint
+import com.linecorp.armeria.server.saml.SamlIdentityProviderConfig
+import com.linecorp.armeria.server.saml.SamlNameIdFormat
+import com.linecorp.armeria.server.saml.SamlSingleSignOnHandler
+import com.linecorp.centraldogma.server.auth.Session
+import com.linecorp.centraldogma.server.internal.api.HttpApiUtil
+import io.netty.handler.codec.http.QueryStringDecoder
+import org.opensaml.core.xml.schema.XSString
+import org.opensaml.messaging.context.MessageContext
+import org.opensaml.saml.common.messaging.context.SAMLBindingContext
+import org.opensaml.saml.saml2.core.Attribute
+import org.opensaml.saml.saml2.core.AuthnRequest
+import org.opensaml.saml.saml2.core.Response
+import org.opensaml.xmlsec.signature.support.SignatureConstants
+import java.io.UnsupportedEncodingException
+import java.net.URLEncoder
+import java.time.Duration
+import java.util.Objects.requireNonNull
+import java.util.Optional
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionStage
+
+data class Idp(
+ val entityId: String,
+ val uri: String,
+ val binding: SamlBindingProtocol = SamlBindingProtocol.HTTP_POST,
+ val signingKey: String,
+ val encryptionKey: String,
+ val subjectLoginNameIdFormat: String?,
+ val attributeLoginName: String?,
+ val attributeGroupName: String?,
+) {
+ @JsonCreator
+ constructor(
+ @JsonProperty("entityId") entityId: String,
+ @JsonProperty("uri") uri: String,
+ @JsonProperty("binding") binding: String?,
+ @JsonProperty("signingKey") signingKey: String?,
+ @JsonProperty("encryptionKey") encryptionKey: String?,
+ @JsonProperty("subjectLoginNameIdFormat") subjectLoginNameIdFormat: String?,
+ @JsonProperty("attributeLoginName") attributeLoginName: String?,
+ @JsonProperty("attributeGroupName") attributeGroupName: String?,
+ ) : this(
+ entityId = entityId,
+ uri = uri,
+ binding = binding?.let(SamlBindingProtocol::valueOf) ?: SamlBindingProtocol.HTTP_POST,
+ signingKey = listOfNotNull(signingKey, entityId).first(),
+ encryptionKey = listOfNotNull(encryptionKey, entityId).first(),
+ subjectLoginNameIdFormat =
+ if (subjectLoginNameIdFormat == null && attributeLoginName == null) {
+ subjectLoginNameIdFormat
+ } else {
+ SamlNameIdFormat.EMAIL.urn()
+ },
+ attributeLoginName =
+ if (subjectLoginNameIdFormat == null && attributeLoginName == null) {
+ null
+ } else {
+ attributeLoginName
+ },
+ attributeGroupName,
+ )
+
+ val endpoint =
+ when (binding) {
+ SamlBindingProtocol.HTTP_POST -> SamlEndpoint.ofHttpPost(uri)
+ SamlBindingProtocol.HTTP_REDIRECT -> SamlEndpoint.ofHttpRedirect(uri)
+ else -> throw IllegalStateException("Failed to get an endpoint of the Idp: $entityId")
+ }
+}
+
+data class SamlAuthProperties(
+ val entityId: String,
+ val hostname: String,
+ val signingKey: String = "signing",
+ val encryptionKey: String = "encryption",
+ val samlMetadata: String,
+ val signatureAlgorithm: String = SignatureConstants.ALGO_ID_SIGNATURE_RSA,
+ val idp: Idp,
+)
+
+class SamlAuthSsoHandler(
+ private val sessionIdGenerator: () -> String,
+ private val loginSessionPropagator: (Session) -> CompletableFuture,
+ private val sessionValidDuration: Duration,
+ private val loginNameNormalizer: (String) -> String,
+ private val subjectLoginNameIdFormat: String?,
+ private val attributeLoginName: String?,
+ private val attributeGroupName: String?,
+) : SamlSingleSignOnHandler {
+ override fun beforeInitiatingSso(
+ ctx: ServiceRequestContext,
+ req: HttpRequest,
+ message: MessageContext,
+ idpConfig: SamlIdentityProviderConfig,
+ ): CompletionStage {
+ val decoder = QueryStringDecoder(req.path(), true)
+ val ref = decoder.parameters()["ref"]
+ if (ref.isNullOrEmpty()) return CompletableFuture.completedFuture(null)
+
+ val relayState = ref[0]
+ if (idpConfig.ssoEndpoint().bindingProtocol() == SamlBindingProtocol.HTTP_REDIRECT &&
+ relayState.length > 80
+ ) {
+ return CompletableFuture.completedFuture(null)
+ }
+
+ message.getSubcontext(SAMLBindingContext::class.java, true).apply {
+ checkNotNull(this) { SAMLBindingContext::class.java.name }
+ this.relayState = relayState
+ }
+
+ return CompletableFuture.completedFuture(null)
+ }
+
+ override fun loginSucceeded(
+ ctx: ServiceRequestContext,
+ req: AggregatedHttpRequest,
+ message: MessageContext,
+ sessionIndex: String?,
+ relayState: String?,
+ ): HttpResponse {
+ val response =
+ requireNonNull(message, "message").message!!
+ val username =
+ Optional.ofNullable(findLoginNameFromSubjects(response))
+ .orElseGet { findLoginNameFromAttributes(response) }
+ if (Strings.isNullOrEmpty(username)) {
+ return loginFailed(
+ ctx,
+ req,
+ message,
+ IllegalStateException("Cannot get a username from the response"),
+ )
+ }
+
+ val sessionId: String = sessionIdGenerator()
+ val samlSession = SamlSession(findGroupsFromAttributes(response))
+ val session =
+ Session(sessionId, loginNameNormalizer(username), sessionValidDuration, samlSession)
+ val redirectionScript =
+ if (!Strings.isNullOrEmpty(relayState)) {
+ try {
+ "window.location.href='/#${URLEncoder.encode(relayState, "UTF-8")}'"
+ } catch (e: UnsupportedEncodingException) {
+ // Should never reach here.
+ throw Error()
+ }
+ } else {
+ "window.location.href='/'"
+ }
+ return HttpResponse.of(
+ loginSessionPropagator(session).thenApply { _ ->
+ HttpResponse.of(
+ HttpStatus.OK,
+ MediaType.HTML_UTF_8,
+ HtmlUtil.getHtmlWithOnload(
+ "localStorage.setItem('sessionId','$sessionId')",
+ redirectionScript,
+ ),
+ )
+ },
+ )
+ }
+
+ private fun findLoginNameFromSubjects(response: Response): String? {
+ if (subjectLoginNameIdFormat.isNullOrEmpty()) return null
+ return response.assertions
+ .map { it.subject.nameID }
+ .firstOrNull { it.format == subjectLoginNameIdFormat }
+ ?.value
+ }
+
+ private fun findLoginNameFromAttributes(response: Response): String? {
+ if (attributeLoginName.isNullOrEmpty()) return null
+ return response.assertions
+ .asSequence()
+ .flatMap { it.attributeStatements.asSequence() }
+ .flatMap { it.attributes.asSequence() }
+ .filter { it.name == attributeLoginName }
+ .firstOrNull()
+ ?.let { attr: Attribute ->
+ val v = attr.attributeValues[0]
+ if (v is XSString) {
+ v.value
+ } else {
+ null
+ }
+ }
+ }
+
+ private fun findGroupsFromAttributes(response: Response): List {
+ if (attributeGroupName.isNullOrEmpty()) return emptyList()
+
+ return response.assertions
+ .asSequence()
+ .flatMap { it.attributeStatements }
+ .flatMap { it.attributes }
+ .filter { it.name == attributeGroupName }
+ .flatMap { it.attributeValues }
+ .filterIsInstance()
+ .mapNotNull { it.value }
+ .toList()
+ }
+
+ override fun loginFailed(
+ ctx: ServiceRequestContext,
+ req: AggregatedHttpRequest,
+ message: MessageContext?,
+ cause: Throwable,
+ ): HttpResponse {
+ val status =
+ if (cause is InvalidSamlRequestException) {
+ HttpStatus.BAD_REQUEST
+ } else {
+ HttpStatus.INTERNAL_SERVER_ERROR
+ }
+ return HttpApiUtil.newResponse(ctx, status, cause)
+ }
+}
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/auth/saml/SamlAuthProviderFactory.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/auth/saml/SamlAuthProviderFactory.kt
new file mode 100644
index 000000000..d27f56e5c
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/server/auth/saml/SamlAuthProviderFactory.kt
@@ -0,0 +1,81 @@
+package com.linecorp.centraldogma.server.auth.saml
+
+import com.google.common.annotations.VisibleForTesting
+import com.linecorp.armeria.common.SessionProtocol
+import com.linecorp.armeria.server.ServerPort
+import com.linecorp.armeria.server.saml.SamlServiceProvider
+import com.linecorp.centraldogma.server.auth.AuthProvider
+import com.linecorp.centraldogma.server.auth.AuthProviderFactory
+import com.linecorp.centraldogma.server.auth.AuthProviderParameters
+import org.opensaml.security.credential.CredentialResolver
+import org.opensaml.security.credential.UsageType
+import org.opensaml.security.credential.impl.StaticCredentialResolver
+import org.opensaml.security.x509.BasicX509Credential
+import java.security.KeyPairGenerator
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+import java.time.Duration
+
+class SamlAuthProviderFactory : AuthProviderFactory {
+ override fun create(parameters: AuthProviderParameters): AuthProvider {
+ val properties =
+ checkNotNull(
+ parameters.authConfig().properties(SamlAuthProperties::class.java),
+ ) { "authentication properties are not specified" }
+
+ val idp = properties.idp
+
+ val samlServiceProvider =
+ SamlServiceProvider.builder()
+ .entityId(properties.entityId)
+ .hostname(properties.hostname)
+ .schemeAndPort(ServerPort(443, SessionProtocol.HTTPS))
+ .authorizer(parameters.authorizer())
+ .ssoHandler(
+ SamlAuthSsoHandler(
+ sessionIdGenerator = { parameters.sessionIdGenerator().get() },
+ loginSessionPropagator = { parameters.loginSessionPropagator().apply(it) },
+ sessionValidDuration =
+ Duration.ofMillis(
+ parameters.authConfig().sessionTimeoutMillis(),
+ ),
+ loginNameNormalizer = { parameters.authConfig().loginNameNormalizer().apply(it) },
+ subjectLoginNameIdFormat = idp.subjectLoginNameIdFormat,
+ attributeLoginName = idp.attributeLoginName,
+ attributeGroupName = idp.attributeGroupName,
+ ),
+ )
+ .credentialResolver(metadataResolver(properties))
+ .signatureAlgorithm(properties.signatureAlgorithm)
+ .apply {
+ idp()
+ .entityId(idp.entityId)
+ .ssoEndpoint(idp.endpoint)
+ .signingKey(idp.signingKey)
+ .encryptionKey(idp.encryptionKey)
+ }
+ .build()
+
+ return SamlAuthProvider(samlServiceProvider)
+ }
+
+ @VisibleForTesting
+ internal fun metadataResolver(properties: SamlAuthProperties): CredentialResolver {
+ val factory = CertificateFactory.getInstance("X.509")
+
+ val certificate =
+ factory.generateCertificate(properties.samlMetadata.byteInputStream()) as X509Certificate
+
+ val keyPairGenerator = KeyPairGenerator.getInstance(certificate.publicKey.algorithm)
+
+ val generateKeyPair = keyPairGenerator.generateKeyPair()
+
+ val credential =
+ BasicX509Credential(certificate).apply {
+ setUsageType(UsageType.SIGNING)
+ setPrivateKey(generateKeyPair.private)
+ }
+
+ return StaticCredentialResolver(credential)
+ }
+}
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/spring/loader/CentralDogmaProperties.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/spring/loader/CentralDogmaProperties.kt
new file mode 100644
index 000000000..5bb6a74ca
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/spring/loader/CentralDogmaProperties.kt
@@ -0,0 +1,190 @@
+/*
+ * 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.spring.loader
+
+import com.google.common.base.Preconditions.checkArgument
+import com.linecorp.armeria.common.SessionProtocol
+import com.linecorp.armeria.server.ClientAddressSource
+import com.linecorp.centraldogma.config.AuthConfig
+import com.linecorp.centraldogma.config.CorsConfig
+import com.linecorp.centraldogma.config.GracefulShutdownTimeout
+import com.linecorp.centraldogma.config.ManagementConfig
+import com.linecorp.centraldogma.config.PluginConfigBase
+import com.linecorp.centraldogma.config.QuotaConfig
+import com.linecorp.centraldogma.config.ReplicationConfigImpl
+import com.linecorp.centraldogma.config.ServerPort
+import com.linecorp.centraldogma.config.TlsConfig
+import com.linecorp.centraldogma.server.AbstractCentralDogmaConfig
+import com.linecorp.centraldogma.server.CorsConfigSpec
+import com.linecorp.centraldogma.server.GracefulShutdownTimeoutSpec
+import com.linecorp.centraldogma.server.ManagementConfigSpec
+import com.linecorp.centraldogma.server.QuotaConfigSpec
+import com.linecorp.centraldogma.server.ReplicationConfig
+import com.linecorp.centraldogma.server.TlsConfigSpec
+import com.linecorp.centraldogma.server.ZoneConfigSpec
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec
+import com.linecorp.centraldogma.server.internal.storage.repository.RepositoryCache.validateCacheSpec
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.boot.context.properties.NestedConfigurationProperty
+import java.io.File
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.util.Optional
+import java.util.function.Predicate
+
+@ConfigurationProperties("central-dogma")
+data class CentralDogmaProperties(
+ private val dataDir: File,
+ @NestedConfigurationProperty
+ private val ports: List,
+ @NestedConfigurationProperty
+ private val tls: TlsConfig? = null,
+ private val trustedProxyAddresses: List? = null,
+ private val clientAddressSources: List? = null,
+ private val numWorkers: Int? = null,
+ private val maxNumConnections: Int? = null,
+ private val requestTimeoutMillis: Long? = null,
+ private val idleTimeoutMillis: Long? = null,
+ private val maxFrameLength: Int? = null,
+ /**
+ * default value from [com.linecorp.centraldogma.server.CentralDogmaBuilder.DEFAULT_NUM_REPOSITORY_WORKERS]
+ */
+ private val numRepositoryWorkers: Int = 16,
+ private val repositoryCacheSpec: String = "maximumWeight=134217728,expireAfterAccess=5m",
+ private val maxRemovedRepositoryAgeMillis: Long = 604_800_000,
+ @NestedConfigurationProperty
+ private val gracefulShutdownTimeout: GracefulShutdownTimeout? = null,
+ private val webAppEnabled: Boolean = true,
+ private val webAppTitle: String? = null,
+ @NestedConfigurationProperty
+ private val replication: ReplicationConfigImpl,
+ private val csrfTokenRequiredForThrift: Boolean = true,
+ private val accessLogFormat: String? = null,
+ @NestedConfigurationProperty
+ private val authentication: AuthConfig? = null,
+ @NestedConfigurationProperty
+ private val writeQuotaPerRepository: QuotaConfig? = null,
+ @NestedConfigurationProperty
+ private val cors: CorsConfig? = null,
+ private val pluginConfigs: List? = null,
+ @NestedConfigurationProperty
+ private val management: ManagementConfig? = null,
+) : AbstractCentralDogmaConfig(pluginConfigs) {
+ private val trustedProxyAddressPredicate: Predicate
+
+ private val clientAddressSourceList: List
+
+ private val armeriaServerPorts: List =
+ ports.map { serverPort ->
+ val localAddress =
+ if (serverPort.localAddress.host == "*") {
+ InetSocketAddress(serverPort.localAddress.port)
+ } else {
+ InetSocketAddress(serverPort.localAddress.host, serverPort.localAddress.port)
+ }
+
+ val protocols =
+ serverPort.protocols.map {
+ SessionProtocol.of(it)
+ }.toSet()
+
+ com.linecorp.armeria.server.ServerPort(localAddress, protocols)
+ }
+
+ init {
+ checkArgument(
+ this.numRepositoryWorkers > 0,
+ "numRepositoryWorkers: %s (expected: > 0)",
+ this.numRepositoryWorkers,
+ )
+ checkArgument(
+ this.maxRemovedRepositoryAgeMillis >= 0,
+ "maxRemovedRepositoryAgeMillis: %s (expected: >= 0)",
+ this.maxRemovedRepositoryAgeMillis,
+ )
+
+ validateCacheSpec(repositoryCacheSpec)
+
+ val hasTrustedProxyAddrCfg = !trustedProxyAddresses.isNullOrEmpty()
+
+ trustedProxyAddressPredicate =
+ if (hasTrustedProxyAddrCfg) {
+ toTrustedProxyAddressPredicate(trustedProxyAddresses!!)
+ } else {
+ Predicate { false }
+ }
+
+ clientAddressSourceList =
+ toClientAddressSourceList(
+ clientAddressSources, hasTrustedProxyAddrCfg,
+ armeriaServerPorts.any(com.linecorp.armeria.server.ServerPort::hasProxyProtocol),
+ )
+ }
+
+ override fun dataDir(): File = dataDir
+
+ override fun ports(): List = armeriaServerPorts
+
+ override fun tls(): TlsConfigSpec? = tls
+
+ override fun trustedProxyAddresses(): List? = trustedProxyAddresses
+
+ override fun clientAddressSources(): List? = clientAddressSources
+
+ override fun numWorkers(): Optional = Optional.ofNullable(numWorkers)
+
+ override fun maxNumConnections(): Optional = Optional.ofNullable(maxNumConnections)
+
+ override fun requestTimeoutMillis(): Optional = Optional.ofNullable(requestTimeoutMillis)
+
+ override fun idleTimeoutMillis(): Optional = Optional.ofNullable(idleTimeoutMillis)
+
+ override fun maxFrameLength(): Optional = Optional.ofNullable(maxFrameLength)
+
+ override fun numRepositoryWorkers(): Int = numRepositoryWorkers
+
+ override fun maxRemovedRepositoryAgeMillis(): Long = maxRemovedRepositoryAgeMillis
+
+ override fun repositoryCacheSpec(): String = repositoryCacheSpec
+
+ override fun gracefulShutdownTimeout(): Optional = Optional.ofNullable(gracefulShutdownTimeout)
+
+ override fun isWebAppEnabled(): Boolean = webAppEnabled
+
+ override fun webAppTitle(): String? = webAppTitle
+
+ override fun replicationConfig(): ReplicationConfig = replication
+
+ override fun isCsrfTokenRequiredForThrift(): Boolean = this.csrfTokenRequiredForThrift
+
+ override fun accessLogFormat(): String? = accessLogFormat
+
+ override fun authConfig(): AuthConfigSpec? = authentication
+
+ override fun writeQuotaPerRepository(): QuotaConfigSpec? = writeQuotaPerRepository
+
+ override fun corsConfig(): CorsConfigSpec? = cors
+
+ override fun managementConfig(): ManagementConfigSpec? = management
+
+ // TODO 이따 구현
+ override fun zone(): ZoneConfigSpec? = null
+
+ override fun trustedProxyAddressPredicate(): Predicate = trustedProxyAddressPredicate
+
+ override fun clientAddressSourceList(): List = clientAddressSourceList
+}
diff --git a/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/spring/loader/CentralDogmaSpringLoader.kt b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/spring/loader/CentralDogmaSpringLoader.kt
new file mode 100644
index 000000000..317f88f5c
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/kotlin/com/linecorp/centraldogma/spring/loader/CentralDogmaSpringLoader.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.spring.loader
+
+import com.linecorp.centraldogma.server.CentralDogma
+import com.linecorp.centraldogma.server.CentralDogma.TokenAuthorizerCreator
+import com.linecorp.centraldogma.server.CentralDogmaConfigSpec
+import com.linecorp.centraldogma.server.CustomConfigBindHandlerConfiguration
+import com.linecorp.centraldogma.server.auth.AuthProviderParameters
+import com.linecorp.centraldogma.server.auth.saml.SessionGroupTokenAuthorizer
+import com.linecorp.centraldogma.server.command.Command
+import com.linecorp.centraldogma.server.internal.api.auth.ApplicationTokenAuthorizer
+import io.micrometer.core.instrument.MeterRegistry
+import org.springframework.beans.factory.DisposableBean
+import org.springframework.beans.factory.InitializingBean
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.boot.context.properties.EnableConfigurationProperties
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Import
+
+class CentralDogmaManager(val centralDogma: CentralDogma) : DisposableBean, InitializingBean {
+ override fun afterPropertiesSet() {
+ centralDogma.start().join()
+ }
+
+ override fun destroy() {
+ centralDogma.stop().join()
+ }
+}
+
+@ConfigurationProperties("central-dogma-admin")
+data class CentralDogmaAdminProperties(
+ val adminGroupNames: Set = emptySet(),
+)
+
+@Import(CustomConfigBindHandlerConfiguration::class)
+@EnableConfigurationProperties(
+ CentralDogmaProperties::class,
+ CentralDogmaAdminProperties::class,
+)
+@SpringBootApplication
+class CentralDogmaSpringLoader {
+ @Bean
+ fun centralDogma(
+ properties: CentralDogmaProperties,
+ adminProperties: CentralDogmaAdminProperties,
+ meterRegistry: MeterRegistry,
+ ): CentralDogmaManager {
+ val authConfig = properties.authConfig()
+
+ Thread.sleep(10000)
+ val tokenAuthorizerCreator =
+ TokenAuthorizerCreator { mds, sessionManager ->
+ ApplicationTokenAuthorizer(mds::findTokenBySecret)
+ .orElse(SessionGroupTokenAuthorizer(sessionManager, adminProperties.adminGroupNames))
+ }
+ val authProviderCreator =
+ if (authConfig == null) {
+ null
+ } else {
+ CentralDogma.AuthProviderCreator { commandExecutor, sessionManager, mds ->
+ checkNotNull(sessionManager) { "SessionManager is null" }
+
+ val parameters =
+ AuthProviderParameters(
+ tokenAuthorizerCreator.create(mds, sessionManager),
+ properties,
+ sessionManager::generateSessionId,
+ { commandExecutor.execute(Command.createSession(it)) },
+ { commandExecutor.execute(Command.removeSession(it)) },
+ )
+
+ authConfig.factory().create(parameters)
+ }
+ }
+
+ val centralDogmaConfig =
+ object : CentralDogmaConfigSpec by properties {
+ override fun toString(): String = "Contains sensitive values. Please use actuator instead."
+ }
+
+ return CentralDogmaManager(
+ CentralDogma.forConfig(centralDogmaConfig, meterRegistry)
+ .authProviderCreator(authProviderCreator)
+ .tokenAuthorizerCreator(tokenAuthorizerCreator),
+ )
+ }
+}
+
+fun main(args: Array) {
+ runApplication(*args)
+}
diff --git a/cloud-native/boot-loader/src/main/resources/application.yaml b/cloud-native/boot-loader/src/main/resources/application.yaml
new file mode 100644
index 000000000..a2c9a388b
--- /dev/null
+++ b/cloud-native/boot-loader/src/main/resources/application.yaml
@@ -0,0 +1,28 @@
+---
+central-dogma:
+ dataDir: "./data"
+ ports:
+ - local-address:
+ host: '*'
+ port: 36462
+ protocols:
+ - http
+ num-repository-workers: 16
+ repository-cache-spec: "maximumWeight=134217728,expireAfterAccess=5m"
+ graceful-shutdown-timeout:
+ quiet-period-millis: 1000
+ timeout-millis: 10000
+ web-app-enabled: true
+ replication:
+ method: NONE
+ access-log-format: common
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: '*'
+ endpoint:
+ health:
+ probes:
+ enabled: true
diff --git a/cloud-native/boot-loader/src/test/kotlin/com/linecorp/centraldogma/spring/loader/ConfigurationPropertiesTest.kt b/cloud-native/boot-loader/src/test/kotlin/com/linecorp/centraldogma/spring/loader/ConfigurationPropertiesTest.kt
new file mode 100644
index 000000000..40af6f43c
--- /dev/null
+++ b/cloud-native/boot-loader/src/test/kotlin/com/linecorp/centraldogma/spring/loader/ConfigurationPropertiesTest.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.spring.loader
+
+import assertk.all
+import assertk.assertThat
+import assertk.assertions.containsOnly
+import assertk.assertions.hasSize
+import assertk.assertions.isEqualTo
+import assertk.assertions.isInstanceOf
+import assertk.assertions.isNotNull
+import assertk.assertions.isNull
+import assertk.assertions.prop
+import com.linecorp.armeria.common.SessionProtocol
+import com.linecorp.armeria.server.ServerPort
+import com.linecorp.centraldogma.config.GracefulShutdownTimeout
+import com.linecorp.centraldogma.config.MirroringServicePluginConfig
+import com.linecorp.centraldogma.config.ZooKeeperReplicationConfig
+import com.linecorp.centraldogma.server.CentralDogmaConfigSpec
+import com.linecorp.centraldogma.server.CustomConfigBindHandlerConfiguration
+import com.linecorp.centraldogma.server.ReplicationMethod
+import com.linecorp.centraldogma.server.ZooKeeperReplicationConfigSpec
+import com.linecorp.centraldogma.server.ZooKeeperServerConfigSpec
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec
+import com.linecorp.centraldogma.server.auth.saml.Idp
+import com.linecorp.centraldogma.server.auth.saml.SamlAuthProperties
+import com.linecorp.centraldogma.server.auth.saml.SamlAuthProviderFactory
+import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfigSpec
+import org.junit.jupiter.api.Test
+import org.opensaml.xmlsec.signature.support.SignatureConstants
+import org.springframework.boot.context.properties.EnableConfigurationProperties
+import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.test.context.TestConstructor
+import org.springframework.test.context.TestPropertySource
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig
+import java.io.File
+import java.util.Optional
+
+@SpringJUnitConfig(
+ TestConfiguration::class,
+ initializers = [ConfigDataApplicationContextInitializer::class],
+)
+@TestPropertySource(properties = ["spring.config.location=classpath:full-config.yaml"])
+@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
+class ConfigurationPropertiesTest(
+ private val centralDogmaConfig: CentralDogmaConfigSpec,
+) {
+ @Test
+ fun test() {
+ println(centralDogmaConfig)
+
+ assertThat(centralDogmaConfig).all {
+ prop(CentralDogmaConfigSpec::dataDir).isEqualTo(File("./data"))
+ prop(CentralDogmaConfigSpec::ports).isEqualTo(listOf(ServerPort(36462, SessionProtocol.HTTP)))
+ prop(CentralDogmaConfigSpec::numRepositoryWorkers).isEqualTo(16)
+ prop(CentralDogmaConfigSpec::repositoryCacheSpec).isEqualTo("maximumWeight=134217728,expireAfterAccess=5m")
+ prop(CentralDogmaConfigSpec::gracefulShutdownTimeout)
+ .isEqualTo(Optional.of(GracefulShutdownTimeout(1000L, 10000L)))
+
+ prop("isWebAppEnabled") { it.isWebAppEnabled }.isEqualTo(true)
+
+ prop(CentralDogmaConfigSpec::replicationConfig)
+ .isInstanceOf()
+ .all {
+ prop(ZooKeeperReplicationConfigSpec::method).isEqualTo(ReplicationMethod.ZOOKEEPER)
+ prop(ZooKeeperReplicationConfigSpec::secret).isEqualTo("secretKey")
+ prop(ZooKeeperReplicationConfigSpec::servers).all {
+ hasSize(1)
+ prop("1") { it[1] }.isNotNull().all {
+ prop(ZooKeeperServerConfigSpec::host).isEqualTo("127.0.0.1")
+ prop(ZooKeeperServerConfigSpec::quorumPort).isEqualTo(2030)
+ prop(ZooKeeperServerConfigSpec::electionPort).isEqualTo(2345)
+ prop(ZooKeeperServerConfigSpec::clientPort).isEqualTo(2345)
+ prop(ZooKeeperServerConfigSpec::groupId).isNull()
+ prop(ZooKeeperServerConfigSpec::weight).isEqualTo(1)
+ }
+ }
+ }
+ prop(CentralDogmaConfigSpec::accessLogFormat).isEqualTo("common")
+ prop(CentralDogmaConfigSpec::pluginConfigs).all {
+ hasSize(1)
+ prop("0") { it.first() }.isInstanceOf().all {
+ prop(MirroringServicePluginConfigSpec::numMirroringThreads).isEqualTo(10)
+ prop(MirroringServicePluginConfigSpec::maxNumFilesPerMirror).isEqualTo(10)
+ prop(MirroringServicePluginConfigSpec::maxNumBytesPerMirror)
+ .isEqualTo(MirroringServicePluginConfigSpec.DEFAULT_MAX_NUM_BYTES_PER_MIRROR)
+ }
+ }
+ prop(CentralDogmaConfigSpec::authConfig).isNotNull().all {
+ prop(AuthConfigSpec::factoryClassName).isEqualTo(SamlAuthProviderFactory::class.qualifiedName)
+ prop(AuthConfigSpec::factory).isInstanceOf()
+ prop(AuthConfigSpec::systemAdministrators).all {
+ hasSize(1)
+ containsOnly("test@linecorp.com")
+ }
+ prop("properties") { it.properties(SamlAuthProperties::class.java) }.isNotNull().all {
+ prop(SamlAuthProperties::entityId).isEqualTo("https://dogma.com")
+ prop(SamlAuthProperties::hostname).isEqualTo("dogma.com")
+ prop(SamlAuthProperties::samlMetadata).isNotNull()
+ prop(SamlAuthProperties::signatureAlgorithm).isEqualTo(SignatureConstants.ALGO_ID_SIGNATURE_RSA)
+ prop(SamlAuthProperties::idp).all {
+ prop(Idp::entityId).isEqualTo("dogma-saml")
+ prop(Idp::uri).isEqualTo("https://dogma.com/signon")
+ }
+ }
+ }
+ }
+
+ SamlAuthProviderFactory()
+ .metadataResolver(centralDogmaConfig.authConfig()!!.properties(SamlAuthProperties::class.java)!!)
+ }
+}
+
+@Import(CustomConfigBindHandlerConfiguration::class)
+@EnableConfigurationProperties(CentralDogmaProperties::class)
+@Configuration(proxyBeanMethods = false)
+class TestConfiguration
diff --git a/cloud-native/boot-loader/src/test/resources/full-config.yaml b/cloud-native/boot-loader/src/test/resources/full-config.yaml
new file mode 100644
index 000000000..3885cdd63
--- /dev/null
+++ b/cloud-native/boot-loader/src/test/resources/full-config.yaml
@@ -0,0 +1,104 @@
+---
+central-dogma:
+ dataDir: "./data"
+ ports:
+ - local-address:
+ host: '*'
+ port: 36462
+ protocols:
+ - http
+ num-repository-workers: 16
+ repository-cache-spec: "maximumWeight=134217728,expireAfterAccess=5m"
+ graceful-shutdown-timeout:
+ quiet-period-millis: 1000
+ timeout-millis: 10000
+ web-app-enabled: true
+ replication:
+ method: ZOOKEEPER
+ secret: secretKey
+ servers:
+ '1':
+ host: 127.0.0.1
+ quorumPort: 2030
+ electionPort: 2345
+ clientPort: 2345
+ groupId: null
+ weight: 1
+ access-log-format: common
+ pluginConfigs:
+ - enabled: true
+ type: com.linecorp.centraldogma.config.MirroringServicePluginConfig
+ num-mirroring-threads: 10
+ max-num-files-per-mirror: 10
+ authentication:
+ administrators: [ "test@linecorp.com" ]
+ factoryClassName: com.linecorp.centraldogma.server.auth.saml.SamlAuthProviderFactory
+ properties:
+ entityId: https://dogma.com
+ hostname: dogma.com
+ signatureAlgorithm: http://www.w3.org/2000/09/xmldsig#rsa-sha1
+ samlMetadata: |-
+ Bag Attributes
+ friendlyName: encryption
+ localKeyID: 54 69 6D 65 20 31 37 32 37 32 33 31 35 36 33 35 34 34
+ subject=C=Unknown, ST=Unknown, L=Unknown, O=Unknown, OU=Unknown, CN=Unknown
+ issuer=C=Unknown, ST=Unknown, L=Unknown, O=Unknown, OU=Unknown, CN=Unknown
+ -----BEGIN CERTIFICATE-----
+ MIIDdzCCAl+gAwIBAgIEbiT6NTANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdV
+ bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD
+ VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du
+ MB4XDTE4MTAwNTA2MjcyNVoXDTE5MDEwMzA2MjcyNVowbDEQMA4GA1UEBhMHVW5r
+ bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE
+ ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC
+ ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK9V1vYacLGyBk3owicRYamd
+ ttk/VJ0Y79BhjJobpMCeP8oyqiLNoeQXh3q5MI3YRSrSewtJKweun1E2rNHWveuk
+ 6+wIEtJ4HChAorNLkI4R0OG99Qe/kltRsr3Q1GDycuAFi9jWTN221S40Zj9kIadn
+ 3dtwXUrg2gSIdFW7+mhTuODKRzBaZAO/5OZN7600ZL9j7IjtldpIZCum3FIIXTQD
+ 6z88Ymfbgs8eMQzNmXvC04ULWjgD5KzbJX5W2df1Yg3a4QJBcTIyD2NBJKL1410R
+ t4/tHd7bhcJ+MXU23qShWkxThP8n4akFZ38uslNQeI86F2wWT5uxyrsFmVXtAqMC
+ AwEAAaMhMB8wHQYDVR0OBBYEFLKyum/cqOwnnMFJdc9SRa9j5H8oMA0GCSqGSIb3
+ DQEBBQUAA4IBAQBYLxmsaljw9qLFxWBjaQBKv1xSzQIwL01zaXntkS7zhffhdElS
+ fn9ril18N32M6K/l4wkjY57tUXNyDr6WsYouP+Xv8FilwVM3GKW6Jj3ED5rtkRzr
+ jWU1t4Si8rLSVClIRw/4UTy7BLonhE47DI/3jGIC60jxrE8fomtHeusioXn1NYfK
+ Qvrfjd0ZldhBz1ZEU7Dlx6+vQ4YfonFUWDByUJUobF6NIWP+kmdRVV6fX4fP6+Yq
+ ciPzN59IELyxvnvCSBvuE8ihIzT6zg5bundOU+ATOvTIe+94tXmFwU3+0ifHdmas
+ fAzZ+eEXN6iz7yZLHLQg5FuRUjvJRWQmSkfe
+ -----END CERTIFICATE-----
+ Bag Attributes
+ friendlyName: signing
+ localKeyID: 54 69 6D 65 20 31 37 32 37 32 33 31 35 36 33 36 33 33
+ subject=C=Unknown, ST=Unknown, L=Unknown, O=Unknown, OU=Unknown, CN=Unknown
+ issuer=C=Unknown, ST=Unknown, L=Unknown, O=Unknown, OU=Unknown, CN=Unknown
+ -----BEGIN CERTIFICATE-----
+ MIIDdzCCAl+gAwIBAgIEIXeHCDANBgkqhkiG9w0BAQUFADBsMRAwDgYDVQQGEwdV
+ bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD
+ VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du
+ MB4XDTE4MTAwNTA2Mjc0M1oXDTE5MDEwMzA2Mjc0M1owbDEQMA4GA1UEBhMHVW5r
+ bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE
+ ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC
+ ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKubyc0G4ekrNBd131G1+WCS
+ QIMLYhunLfUjz+46PngDWWt7nS21RbHvV8eDirpc3wEuEfTYP4FpwF6etTHB92u6
+ X+1oSboiMfx3EiEDA+3ziBinzhBP9lL6Xd8x0v7hPCTtlWrwBY5RMSkvBT5N4khX
+ APQwCgYmwK2UBoTEPcr3O/s2ysbnpu4fa5SAX1mxQhUAeZMRgG7j05jP9Np8KoIH
+ W0zPsF4W+n+09pbG5JYOgVFkM1gVc56PmTUwFa+4D6Wqq/LyRowkXvjyWPoAE6JI
+ jbFhu2UTMKpL6kA9j/48b/ixULOblfV6ahB+kA9Pw9kjMAgcXUTm6hRepmu+gTcC
+ AwEAAaMhMB8wHQYDVR0OBBYEFDXaz1aatlHvzZi355hb+PHMtlK4MA0GCSqGSIb3
+ DQEBBQUAA4IBAQCXcCcwTpj2bWMzrxJOv1YScPT/KzRNlj3Bu6bKh4UW3//wwfO4
+ 1uUhiYL9ont+EFCX6qtxbrWz16fahklaHKIc4AlKvwplwAqw2FUlNKhztmDDC0UM
+ as9o/TUs9rIthOofrfho8FeLULsiKQbZjwSp2o81xooIElBxilkXin+5GjyTQIlL
+ COMJngk/mFdOP5oCvAXVI3YsISgUsDFmR1A1vO0oiUKZY2evruMb7O+Pk2JieyJZ
+ AYI29mmfoDtaBH9pb1OpqEs1dpkTT1EkM51qOQv0wDGV7cksPOHqBNnJHzc9oyuW
+ tCaFCVVxUeL48EUfHU3efqCMw9ba++1LlYAp
+ -----END CERTIFICATE-----
+ idp:
+ entityId: dogma-saml
+ uri: https://dogma.com/signon
+management:
+ endpoints:
+ web:
+ exposure:
+ include: '*'
+ endpoint:
+ health:
+ probes:
+ enabled: true
diff --git a/common/build.gradle b/common/build.gradle
index 41505e6d5..ad4f5b197 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -7,6 +7,7 @@ dependencies {
implementation libs.jackson.core
implementation libs.jackson.databind
implementation libs.jackson.datatype.jsr310
+ implementation libs.jackson.kotlin
// Guava
implementation libs.guava
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/Jackson.java b/common/src/main/java/com/linecorp/centraldogma/internal/Jackson.java
index 5d76f8b05..7a19501be 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/Jackson.java
+++ b/common/src/main/java/com/linecorp/centraldogma/internal/Jackson.java
@@ -51,6 +51,7 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer;
+import com.fasterxml.jackson.module.kotlin.KotlinModule;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.jayway.jsonpath.Configuration;
@@ -79,7 +80,8 @@ public final class Jackson {
prettyMapper.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS);
registerModules(new SimpleModule().addSerializer(Instant.class, InstantSerializer.INSTANCE)
- .addDeserializer(Instant.class, InstantDeserializer.INSTANT));
+ .addDeserializer(Instant.class, InstantDeserializer.INSTANT),
+ new KotlinModule.Builder().build());
}
private static final JsonFactory compactFactory = new JsonFactory(compactMapper);
diff --git a/dependencies.toml b/dependencies.toml
index 14e42f696..83fd15ed8 100644
--- a/dependencies.toml
+++ b/dependencies.toml
@@ -53,6 +53,9 @@ jsoup = "1.18.1" # JSoup is only used for Gradle script.
jmh-core = "1.37"
jmh-gradle-plugin = "0.7.2"
jxr = "0.2.1"
+ktlint-gradle-plugin = "12.1.1"
+kotlin = "1.9.23"
+kotlin-coroutine = "1.8.1"
kubernetes-client = "6.13.4"
logback12 = { strictly = "1.2.13" }
logback15 = { strictly = "1.5.7" }
@@ -222,6 +225,8 @@ module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310"
# Only used for testing. See JacksonRequestConverterFunctionTest for more information.
[libraries.jackson-module-scala]
module = "com.fasterxml.jackson.module:jackson-module-scala_3"
+[libraries.jackson-kotlin]
+module = "com.fasterxml.jackson.module:jackson-module-kotlin"
[libraries.javassist]
module = "org.javassist:javassist"
@@ -454,6 +459,14 @@ version.ref = "spring-boot3"
module = "org.springframework.boot:spring-boot-starter"
version.ref = "spring-boot3"
javadocs = "https://docs.spring.io/spring/docs/current/javadoc-api/"
+[libraries.spring-boot3-starter-actuator]
+module = "org.springframework.boot:spring-boot-starter-actuator"
+version.ref = "spring-boot3"
+javadocs = "https://docs.spring.io/spring/docs/current/javadoc-api/"
+[libraries.spring-boot3-starter-web]
+module = "org.springframework.boot:spring-boot-starter-web"
+version.ref = "spring-boot3"
+javadocs = "https://docs.spring.io/spring/docs/current/javadoc-api/"
[libraries.spring-boot3-starter-test]
module = "org.springframework.boot:spring-boot-starter-test"
version.ref = "spring-boot3"
@@ -485,9 +498,15 @@ exclusions = [
[plugins]
docker = { id = "com.bmuschko.docker-remote-api", version.ref = "docker" }
download = { id = "de.undercouch.download", version.ref = "download" }
+jib = { id = "com.google.cloud.tools.jib", version = "3.2.1" }
jmh = { id = "me.champeau.jmh", version.ref = "jmh-gradle-plugin" }
jxr = { id = "net.davidecavestro.gradle.jxr", version.ref = "jxr" }
nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus-publish-plugin" }
node-gradle = { id = "com.github.node-gradle.node", version.ref = "node-gradle-plugin" }
osdetector = { id = "com.google.osdetector", version.ref = "osdetector" }
sphinx = { id = "kr.motd.sphinx", version.ref = "sphinx" }
+kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle-plugin" }
+spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot3" }
+kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
+
diff --git a/it/server/src/test/java/com/linecorp/centraldogma/it/ReplicationWriteQuotaTest.java b/it/server/src/test/java/com/linecorp/centraldogma/it/ReplicationWriteQuotaTest.java
index bacf598aa..67d54efbd 100644
--- a/it/server/src/test/java/com/linecorp/centraldogma/it/ReplicationWriteQuotaTest.java
+++ b/it/server/src/test/java/com/linecorp/centraldogma/it/ReplicationWriteQuotaTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 LINE Corporation
+ * 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
@@ -46,6 +46,7 @@
import com.linecorp.centraldogma.server.GracefulShutdownTimeout;
import com.linecorp.centraldogma.server.ZooKeeperReplicationConfig;
import com.linecorp.centraldogma.server.ZooKeeperServerConfig;
+import com.linecorp.centraldogma.server.ZooKeeperServerConfigSpec;
import com.linecorp.centraldogma.server.auth.AuthProviderFactory;
import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfig;
import com.linecorp.centraldogma.testing.internal.FlakyTest;
@@ -80,7 +81,7 @@ void setUp() throws IOException {
final int port2 = InstanceSpec.getRandomPort();
final int port3 = InstanceSpec.getRandomPort();
- final Map servers = randomServerConfigs(3);
+ final Map servers = randomServerConfigs(3);
final CompletableFuture r1 = startNewReplicaWithRetries(port1, 1, servers);
final CompletableFuture r2 = startNewReplicaWithRetries(port2, 2, servers);
@@ -101,8 +102,8 @@ void setUp() throws IOException {
.build();
}
- private static Map randomServerConfigs(int numReplicas) {
- final ImmutableMap.Builder builder =
+ private static Map randomServerConfigs(int numReplicas) {
+ final ImmutableMap.Builder builder =
ImmutableMap.builderWithExpectedSize(numReplicas);
for (int i = 0; i < numReplicas; i++) {
final int zkQuorumPort = InstanceSpec.getRandomPort();
@@ -116,7 +117,7 @@ private static Map randomServerConfigs(int numRe
}
private static CompletableFuture startNewReplicaWithRetries(
- int port, int serverId, Map servers) throws IOException {
+ int port, int serverId, Map servers) throws IOException {
final AtomicReference> futureRef = new AtomicReference<>();
await().pollInSameThread().pollInterval(Duration.ofSeconds(1)).untilAsserted(() -> {
assertThatCode(() -> {
@@ -127,7 +128,7 @@ private static CompletableFuture startNewReplicaWithRetries(
}
private static CompletableFuture startNewReplica(
- int port, int serverId, Map servers) throws IOException {
+ int port, int serverId, Map servers) throws IOException {
return new CentralDogmaBuilder(tempDir.newFolder().toFile())
.port(port, SessionProtocol.HTTP)
.systemAdministrators(TestAuthMessageUtil.USERNAME)
diff --git a/it/server/src/test/java/com/linecorp/centraldogma/it/TestAllReplicasPlugin.java b/it/server/src/test/java/com/linecorp/centraldogma/it/TestAllReplicasPlugin.java
index 280f2858e..2ea2a869c 100644
--- a/it/server/src/test/java/com/linecorp/centraldogma/it/TestAllReplicasPlugin.java
+++ b/it/server/src/test/java/com/linecorp/centraldogma/it/TestAllReplicasPlugin.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 LINE Corporation
+ * 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
@@ -21,6 +21,8 @@
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.util.UnmodifiableFuture;
import com.linecorp.centraldogma.server.plugin.AllReplicasPlugin;
+import com.linecorp.centraldogma.server.plugin.NoopPluginConfig;
+import com.linecorp.centraldogma.server.plugin.PluginConfig;
import com.linecorp.centraldogma.server.plugin.PluginContext;
import com.linecorp.centraldogma.server.plugin.PluginInitContext;
@@ -43,8 +45,8 @@ public CompletionStage stop(PluginContext context) {
}
@Override
- public Class> configType() {
+ public Class extends PluginConfig> configType() {
// Return the plugin class itself because it does not have a configuration.
- return TestAllReplicasPlugin.class;
+ return NoopPluginConfig.class;
}
}
diff --git a/it/server/src/test/java/com/linecorp/centraldogma/it/WriteQuotaTestBase.java b/it/server/src/test/java/com/linecorp/centraldogma/it/WriteQuotaTestBase.java
index a99a685ec..3a8bbb1ea 100644
--- a/it/server/src/test/java/com/linecorp/centraldogma/it/WriteQuotaTestBase.java
+++ b/it/server/src/test/java/com/linecorp/centraldogma/it/WriteQuotaTestBase.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 LINE Corporation
+ * 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
@@ -42,6 +42,7 @@
import com.linecorp.centraldogma.common.PushResult;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.server.QuotaConfig;
+import com.linecorp.centraldogma.server.QuotaConfigSpec;
import com.linecorp.centraldogma.server.metadata.ProjectMetadata;
abstract class WriteQuotaTestBase {
@@ -65,7 +66,7 @@ void updateWriteQuota() throws Exception {
/// update write quota to 2qps
QuotaConfig writeQuota = new QuotaConfig(2, 1);
- QuotaConfig updated = updateWriteQuota(webClient(), repositoryName, writeQuota);
+ QuotaConfigSpec updated = updateWriteQuota(webClient(), repositoryName, writeQuota);
assertThat(updated).isEqualTo(writeQuota);
// Wait for releasing previously acquired locks
@@ -94,8 +95,8 @@ void updateWriteQuota() throws Exception {
assertThat(CompletableFutures.allAsList(futures4).join()).hasSize(8);
}
- private static QuotaConfig updateWriteQuota(
- WebClient systemAdminClient, String repoName, QuotaConfig writeQuota)
+ private static QuotaConfigSpec updateWriteQuota(WebClient systemAdminClient, String repoName,
+ QuotaConfig writeQuota)
throws JsonProcessingException {
final String updatePath = "/api/v1/metadata/test_prj/repos/" + repoName + "/quota/write";
final String content = mapper.writeValueAsString(writeQuota);
diff --git a/it/zone-leader-plugin/src/test/java/com/linecorp/centraldogma/it/zoneleader/ZoneLeaderPluginTest.java b/it/zone-leader-plugin/src/test/java/com/linecorp/centraldogma/it/zoneleader/ZoneLeaderPluginTest.java
index 7489c0ca8..d8bf17e9b 100644
--- a/it/zone-leader-plugin/src/test/java/com/linecorp/centraldogma/it/zoneleader/ZoneLeaderPluginTest.java
+++ b/it/zone-leader-plugin/src/test/java/com/linecorp/centraldogma/it/zoneleader/ZoneLeaderPluginTest.java
@@ -31,9 +31,11 @@
import com.linecorp.armeria.common.util.UnmodifiableFuture;
import com.linecorp.centraldogma.server.CentralDogmaBuilder;
-import com.linecorp.centraldogma.server.CentralDogmaConfig;
+import com.linecorp.centraldogma.server.CentralDogmaConfigSpec;
import com.linecorp.centraldogma.server.ZoneConfig;
+import com.linecorp.centraldogma.server.plugin.NoopPluginConfig;
import com.linecorp.centraldogma.server.plugin.Plugin;
+import com.linecorp.centraldogma.server.plugin.PluginConfig;
import com.linecorp.centraldogma.server.plugin.PluginContext;
import com.linecorp.centraldogma.server.plugin.PluginTarget;
import com.linecorp.centraldogma.testing.internal.CentralDogmaReplicationExtension;
@@ -119,7 +121,7 @@ private ZoneLeaderTestPlugin(int serverId) {
}
@Override
- public PluginTarget target(CentralDogmaConfig config) {
+ public PluginTarget target(CentralDogmaConfigSpec config) {
return PluginTarget.ZONE_LEADER_ONLY;
}
@@ -140,8 +142,8 @@ public CompletionStage stop(PluginContext context) {
}
@Override
- public Class> configType() {
- return getClass();
+ public Class extends PluginConfig> configType() {
+ return NoopPluginConfig.class;
}
}
}
diff --git a/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlAuthProviderFactory.java b/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlAuthProviderFactory.java
index 0e2fecd21..5f2e5f938 100644
--- a/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlAuthProviderFactory.java
+++ b/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlAuthProviderFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * 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
@@ -29,7 +29,7 @@
import com.linecorp.armeria.server.saml.KeyStoreCredentialResolverBuilder;
import com.linecorp.armeria.server.saml.SamlServiceProvider;
import com.linecorp.armeria.server.saml.SamlServiceProviderBuilder;
-import com.linecorp.centraldogma.server.auth.AuthConfig;
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec;
import com.linecorp.centraldogma.server.auth.AuthProvider;
import com.linecorp.centraldogma.server.auth.AuthProviderFactory;
import com.linecorp.centraldogma.server.auth.AuthProviderParameters;
@@ -73,14 +73,14 @@ public AuthProvider create(AuthProviderParameters parameters) {
}
}
- private static SamlAuthProperties getProperties(AuthConfig authConfig) {
+ private static SamlAuthProperties getProperties(AuthConfigSpec authConfig) {
try {
final SamlAuthProperties p = authConfig.properties(SamlAuthProperties.class);
checkState(p != null, "authentication properties are not specified");
return p;
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to get properties from " +
- AuthConfig.class.getSimpleName(), e);
+ authConfig.getClass().getSimpleName(), e);
}
}
diff --git a/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlAuthSsoHandler.java b/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlAuthSsoHandler.java
index 34cda7480..90bc9955f 100644
--- a/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlAuthSsoHandler.java
+++ b/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlAuthSsoHandler.java
@@ -123,7 +123,11 @@ public HttpResponse loginSucceeded(ServiceRequestContext ctx, AggregatedHttpRequ
final String sessionId = sessionIdGenerator.get();
final Session session =
- new Session(sessionId, loginNameNormalizer.apply(username), sessionValidDuration);
+ new Session(
+ sessionId,
+ loginNameNormalizer.apply(username),
+ sessionValidDuration
+ );
final String redirectionScript;
if (!Strings.isNullOrEmpty(relayState)) {
diff --git a/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlSession.java b/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlSession.java
new file mode 100644
index 000000000..c4e7dc0bd
--- /dev/null
+++ b/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SamlSession.java
@@ -0,0 +1,45 @@
+/*
+ * 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.auth.saml;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * A class representing a SAML session.
+ */
+public class SamlSession implements Serializable {
+ private final List groups;
+
+ /**
+ * Creates a new instance of the `SamlSession` class with the specified list of groups.
+ *
+ * @param groups The list of groups for this session.
+ */
+ public SamlSession(List groups) {
+ this.groups = groups;
+ }
+
+ /**
+ * Returns the list of groups for this session.
+ *
+ * @return The list of groups for this session.
+ */
+ public List groups() {
+ return groups;
+ }
+}
diff --git a/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SessionGroupTokenAuthorizer.java b/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SessionGroupTokenAuthorizer.java
new file mode 100644
index 000000000..b7455d121
--- /dev/null
+++ b/server-auth/saml/src/main/java/com/linecorp/centraldogma/server/auth/saml/SessionGroupTokenAuthorizer.java
@@ -0,0 +1,93 @@
+/*
+ * 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.auth.saml;
+
+import static com.linecorp.centraldogma.server.metadata.User.LEVEL_SYSTEM_ADMIN;
+import static com.linecorp.centraldogma.server.metadata.User.LEVEL_USER;
+import static java.util.Objects.requireNonNull;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletionStage;
+
+import com.linecorp.armeria.common.HttpRequest;
+import com.linecorp.armeria.common.auth.OAuth2Token;
+import com.linecorp.armeria.server.ServiceRequestContext;
+import com.linecorp.armeria.server.auth.AuthTokenExtractors;
+import com.linecorp.armeria.server.auth.Authorizer;
+import com.linecorp.centraldogma.server.auth.SessionManager;
+import com.linecorp.centraldogma.server.internal.admin.auth.AuthUtil;
+import com.linecorp.centraldogma.server.internal.admin.auth.SessionTokenAuthorizer;
+import com.linecorp.centraldogma.server.metadata.User;
+
+/**
+ * A decorator to grant level from saml authentication result. It is one another implementation of
+ * {@link SessionTokenAuthorizer}.
+ */
+public class SessionGroupTokenAuthorizer implements Authorizer {
+
+ private final SessionManager sessionManager;
+ private final Set adminUserGroups;
+
+ /**
+ * Constructs a SessionGroupTokenAuthorizer with the specified session manager and admin user groups
+ * from saml login result.
+ *
+ * @param sessionManager the session manager used for managing sessions
+ * @param adminUserGroups a set of admin user group identifiers
+ * @throws NullPointerException if sessionManager or adminUserGroups is null
+ */
+ public SessionGroupTokenAuthorizer(SessionManager sessionManager, Set adminUserGroups) {
+ this.sessionManager = requireNonNull(sessionManager, "sessionManager");
+ this.adminUserGroups = requireNonNull(adminUserGroups, "adminUserGroups");
+ }
+
+ @Override
+ public CompletionStage authorize(ServiceRequestContext ctx, HttpRequest data) {
+ final OAuth2Token token = AuthTokenExtractors.oAuth2().apply(data.headers());
+ if (token == null) {
+ return completedFuture(false);
+ }
+
+ return sessionManager.get(token.accessToken())
+ .thenApply(session -> {
+ if (session == null) {
+ return false;
+ }
+
+ final SamlSession samlSession;
+ try {
+ samlSession = session.castRawSession();
+ } catch (Exception e) {
+ return false;
+ }
+
+ final List roles =
+ samlSession.groups()
+ .stream().anyMatch(adminUserGroups::contains) ?
+ LEVEL_SYSTEM_ADMIN : LEVEL_USER;
+
+ final String username = session.username();
+
+ final User user = new User(username, roles);
+ ctx.logBuilder().authenticatedUser("user/" + username);
+ AuthUtil.setCurrentUser(ctx, user);
+ return true;
+ });
+ }
+}
diff --git a/server-auth/saml/src/test/java/com/linecorp/centraldogma/server/auth/saml/AuthenticationDeserializationTest.java b/server-auth/saml/src/test/java/com/linecorp/centraldogma/server/auth/saml/AuthenticationDeserializationTest.java
index e508b8f0c..4eb65bcf3 100644
--- a/server-auth/saml/src/test/java/com/linecorp/centraldogma/server/auth/saml/AuthenticationDeserializationTest.java
+++ b/server-auth/saml/src/test/java/com/linecorp/centraldogma/server/auth/saml/AuthenticationDeserializationTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 LINE Corporation
+ * 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
@@ -29,6 +29,7 @@
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.server.ConfigValueConverter;
import com.linecorp.centraldogma.server.auth.AuthConfig;
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec;
class AuthenticationDeserializationTest {
@@ -51,7 +52,7 @@ void authConfigValueConverter() throws Exception {
" \"entityId\": \"dogma-saml\", " +
" \"uri\": \"https://dogma.com/signon\"" +
"}}}}", SamlAuthProviderFactory.class.getName());
- final AuthConfig authConfig = Jackson.readValue(jsonConfig, ParentConfig.class).authConfig;
+ final AuthConfigSpec authConfig = Jackson.readValue(jsonConfig, ParentConfig.class).authConfig;
final SamlAuthProperties properties = authConfig.properties(SamlAuthProperties.class);
assertThat(properties.keyStore().password()).isEqualTo("foo1");
assertThat(properties.keyStore().keyPasswords()).containsOnly(Maps.immutableEntry("dogma", "bar1"));
@@ -77,7 +78,7 @@ public String convert(String prefix, String value) {
}
static class ParentConfig {
- private final AuthConfig authConfig;
+ private final AuthConfigSpec authConfig;
@JsonCreator
ParentConfig(@JsonProperty("authentication") AuthConfig authConfig) {
diff --git a/server-auth/shiro/src/main/java/com/linecorp/centraldogma/server/auth/shiro/ShiroAuthProvider.java b/server-auth/shiro/src/main/java/com/linecorp/centraldogma/server/auth/shiro/ShiroAuthProvider.java
index 5baadd449..29f5f8fa1 100644
--- a/server-auth/shiro/src/main/java/com/linecorp/centraldogma/server/auth/shiro/ShiroAuthProvider.java
+++ b/server-auth/shiro/src/main/java/com/linecorp/centraldogma/server/auth/shiro/ShiroAuthProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * 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
@@ -30,7 +30,7 @@
import org.apache.shiro.util.Factory;
import com.linecorp.armeria.server.HttpService;
-import com.linecorp.centraldogma.server.auth.AuthConfig;
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec;
import com.linecorp.centraldogma.server.auth.AuthProvider;
import com.linecorp.centraldogma.server.auth.Session;
@@ -42,7 +42,7 @@ public final class ShiroAuthProvider implements AuthProvider {
private final HttpService loginApiService;
private final HttpService logoutApiService;
- ShiroAuthProvider(AuthConfig authConfig,
+ ShiroAuthProvider(AuthConfigSpec authConfig,
Ini config,
Supplier sessionIdGenerator,
Function> loginSessionPropagator,
diff --git a/server-auth/shiro/src/main/java/com/linecorp/centraldogma/server/auth/shiro/ShiroAuthProviderFactory.java b/server-auth/shiro/src/main/java/com/linecorp/centraldogma/server/auth/shiro/ShiroAuthProviderFactory.java
index e813fbde3..2c81c1361 100644
--- a/server-auth/shiro/src/main/java/com/linecorp/centraldogma/server/auth/shiro/ShiroAuthProviderFactory.java
+++ b/server-auth/shiro/src/main/java/com/linecorp/centraldogma/server/auth/shiro/ShiroAuthProviderFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * 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
@@ -21,7 +21,7 @@
import org.apache.shiro.config.Ini;
-import com.linecorp.centraldogma.server.auth.AuthConfig;
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec;
import com.linecorp.centraldogma.server.auth.AuthProvider;
import com.linecorp.centraldogma.server.auth.AuthProviderFactory;
import com.linecorp.centraldogma.server.auth.AuthProviderParameters;
@@ -31,7 +31,7 @@
*/
public final class ShiroAuthProviderFactory implements AuthProviderFactory {
- private final Function iniConfigResolver;
+ private final Function iniConfigResolver;
/**
* Creates a new instance with the default {@link Ini} config resolver.
@@ -43,7 +43,7 @@ public ShiroAuthProviderFactory() {
/**
* Creates a new instance with the specified {@code iniConfigResolver}.
*/
- public ShiroAuthProviderFactory(Function iniConfigResolver) {
+ public ShiroAuthProviderFactory(Function iniConfigResolver) {
this.iniConfigResolver = requireNonNull(iniConfigResolver, "iniConfigResolver");
}
@@ -57,7 +57,7 @@ public AuthProvider create(AuthProviderParameters parameters) {
parameters.logoutSessionPropagator());
}
- private static Ini fromConfig(AuthConfig cfg) {
+ private static Ini fromConfig(AuthConfigSpec cfg) {
try {
final String iniPath = cfg.properties(String.class);
return Ini.fromResourcePath(iniPath);
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/AbstractCentralDogmaConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/AbstractCentralDogmaConfig.java
new file mode 100644
index 000000000..056eccafa
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/AbstractCentralDogmaConfig.java
@@ -0,0 +1,110 @@
+/*
+ * 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.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 java.net.InetAddress;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nullable;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+
+import com.linecorp.armeria.common.HttpHeaderNames;
+import com.linecorp.armeria.server.ClientAddressSource;
+import com.linecorp.centraldogma.server.plugin.PluginConfig;
+
+/**
+ * abstract {@link CentralDogma} server configuration.
+ */
+public abstract class AbstractCentralDogmaConfig implements CentralDogmaConfigSpec {
+ private final List pluginConfigs;
+ private final Map, PluginConfig> pluginConfigMap;
+
+ protected AbstractCentralDogmaConfig(@Nullable final List pluginConfigs) {
+ this.pluginConfigs = firstNonNull(pluginConfigs, ImmutableList.of());
+ pluginConfigMap = this.pluginConfigs.stream().collect(
+ toImmutableMap(PluginConfig::getClass, Function.identity()));
+ }
+
+ @Override
+ public List pluginConfigs() {
+ return pluginConfigs;
+ }
+
+ @Override
+ public Map, PluginConfig> pluginConfigMap() {
+ return pluginConfigMap;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Nullable
+ @Override
+ public T pluginConfig(Class pluginClass) {
+ return (T) pluginConfigMap.entrySet()
+ .stream()
+ .filter(entry -> pluginClass.isAssignableFrom(entry.getKey()))
+ .map(Entry::getValue)
+ .findFirst()
+ .orElse(null);
+ }
+
+ protected 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;
+ }
+
+ protected 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();
+ }
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java
index 8d0b05ad3..94a2de8ac 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogma.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * 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
@@ -117,7 +117,7 @@
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.AuthConfigSpec;
import com.linecorp.centraldogma.server.auth.AuthProvider;
import com.linecorp.centraldogma.server.auth.AuthProviderParameters;
import com.linecorp.centraldogma.server.auth.SessionManager;
@@ -221,6 +221,14 @@ public static CentralDogma forConfig(File configFile) throws IOException {
return new CentralDogma(CentralDogmaConfig.load(configFile), Flags.meterRegistry(), ImmutableList.of());
}
+ /**
+ * Creates a new instance from the given configuration instance.
+ */
+ public static CentralDogma forConfig(CentralDogmaConfigSpec config, MeterRegistry meterRegistry) {
+ requireNonNull(config, "config");
+ return new CentralDogma(config, meterRegistry, ImmutableList.of());
+ }
+
private final SettableHealthChecker serverHealth = new SettableHealthChecker(false);
private final CentralDogmaStartStop startStop;
@@ -234,7 +242,7 @@ public static CentralDogma forConfig(File configFile) throws IOException {
@Nullable
private final PluginGroup pluginsForZoneLeaderOnly;
- private final CentralDogmaConfig cfg;
+ private final CentralDogmaConfigSpec cfg;
@Nullable
private volatile ProjectManager pm;
@Nullable
@@ -257,7 +265,46 @@ public static CentralDogma forConfig(File configFile) throws IOException {
@Nullable
private volatile MirrorRunner mirrorRunner;
- CentralDogma(CentralDogmaConfig cfg, MeterRegistry meterRegistry, List plugins) {
+ /**
+ * A functional interface that creates an instance of {@link AuthProvider}.
+ */
+ @FunctionalInterface
+ public interface AuthProviderCreator {
+
+ /**
+ * Creates an instance of AuthProvider.
+ *
+ * @param commandExecutor the command executor
+ * @param sessionManager the session manager, may be null
+ * @param mds the metadata service
+ * @return an instance of AuthProvider, may be null
+ */
+ @Nullable
+ AuthProvider create(CommandExecutor commandExecutor,
+ @Nullable SessionManager sessionManager,
+ MetadataService mds);
+ }
+
+ /**
+ * A functional interface for creating an {@link Authorizer} for HTTP requests.
+ */
+ @FunctionalInterface
+ public interface TokenAuthorizerCreator {
+ /**
+ * Creates an {@link Authorizer} instance based on the provided
+ * {@link MetadataService} and {@link SessionManager}.
+ *
+ * @param mds the metadata service used for authorizing requests
+ * @param sessionManager the session manager for managing user sessions
+ * @return an {@link Authorizer} for HTTP requests
+ */
+ Authorizer create(MetadataService mds, SessionManager sessionManager);
+ }
+
+ private AuthProviderCreator authProviderCreator;
+ private TokenAuthorizerCreator tokenAuthorizerCreator;
+
+ CentralDogma(CentralDogmaConfigSpec cfg, MeterRegistry meterRegistry, List plugins) {
this.cfg = requireNonNull(cfg, "cfg");
pluginGroups = PluginGroup.loadPlugins(CentralDogma.class.getClassLoader(), cfg, plugins);
pluginsForAllReplicas = pluginGroups.get(PluginTarget.ALL_REPLICAS);
@@ -269,6 +316,37 @@ public static CentralDogma forConfig(File configFile) throws IOException {
}
startStop = new CentralDogmaStartStop(pluginsForAllReplicas);
this.meterRegistry = meterRegistry;
+
+ authProviderCreator = this::createAuthProvider;
+ tokenAuthorizerCreator = (mds, sessionManager) -> new ApplicationTokenAuthorizer(mds::findTokenBySecret)
+ .orElse(new SessionTokenAuthorizer(sessionManager,
+ cfg.authConfig().systemAdministrators()));
+ }
+
+ /**
+ * Sets the authentication provider creator.
+ *
+ * @param authProviderCreator the authentication provider creator, or null to use the default creator
+ * @return the current instance of CentralDogma
+ */
+ public CentralDogma authProviderCreator(@Nullable AuthProviderCreator authProviderCreator) {
+ if (authProviderCreator == null) {
+ this.authProviderCreator = this::createAuthProvider;
+ } else {
+ this.authProviderCreator = authProviderCreator;
+ }
+ return this;
+ }
+
+ /**
+ * Sets the TokenAuthorizerCreator for this CentralDogma instance.
+ *
+ * @param tokenAuthorizerCreator the TokenAuthorizerCreator to be set
+ * @return the current CentralDogma instance for method chaining
+ */
+ public CentralDogma tokenAuthorizerCreator(TokenAuthorizerCreator tokenAuthorizerCreator) {
+ this.tokenAuthorizerCreator = tokenAuthorizerCreator;
+ return this;
}
/**
@@ -276,7 +354,7 @@ public static CentralDogma forConfig(File configFile) throws IOException {
*
* @return the {@link CentralDogmaConfig} instance which is used for configuring this {@link CentralDogma}.
*/
- public CentralDogmaConfig config() {
+ public CentralDogmaConfigSpec config() {
return cfg;
}
@@ -364,7 +442,7 @@ public CompletableFuture start() {
public CompletableFuture stop() {
serverHealth.setHealthy(false);
- final Optional gracefulTimeoutOpt = cfg.gracefulShutdownTimeout();
+ final Optional gracefulTimeoutOpt = cfg.gracefulShutdownTimeout();
if (gracefulTimeoutOpt.isPresent()) {
try {
// Sleep 1 second so that clients have some time to redirect traffic according
@@ -580,7 +658,7 @@ private CommandExecutor startCommandExecutor(
@Nullable
private SessionManager initializeSessionManager() throws Exception {
- final AuthConfig authCfg = cfg.authConfig();
+ final AuthConfigSpec authCfg = cfg.authConfig();
if (authCfg == null) {
return null;
}
@@ -620,7 +698,7 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
if (needsTls) {
try {
- final TlsConfig tlsConfig = cfg.tls();
+ final TlsConfigSpec tlsConfig = cfg.tls();
if (tlsConfig != null) {
try (InputStream keyCertChainInputStream = tlsConfig.keyCertChainInputStream();
InputStream keyInputStream = tlsConfig.keyInputStream()) {
@@ -650,7 +728,7 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
final MetadataService mds = new MetadataService(pm, executor);
final WatchService watchService = new WatchService(meterRegistry);
- final AuthProvider authProvider = createAuthProvider(executor, sessionManager, mds);
+ final AuthProvider authProvider = authProviderCreator.create(executor, sessionManager, mds);
final ProjectApiManager projectApiManager = new ProjectApiManager(pm, executor, mds);
configureThriftService(sb, projectApiManager, executor, watchService, mds);
@@ -732,7 +810,7 @@ static HttpFile webAppTitleFile(@Nullable String webAppTitle, String hostname) {
@Nullable
private AuthProvider createAuthProvider(
CommandExecutor commandExecutor, @Nullable SessionManager sessionManager, MetadataService mds) {
- final AuthConfig authCfg = cfg.authConfig();
+ final AuthConfigSpec authCfg = cfg.authConfig();
if (authCfg == null) {
return null;
}
@@ -759,7 +837,7 @@ private CommandExecutor newZooKeeperCommandExecutor(
@Nullable Consumer onReleaseLeadership,
@Nullable Consumer onTakeZoneLeadership,
@Nullable Consumer onReleaseZoneLeadership) {
- final ZooKeeperReplicationConfig zkCfg = (ZooKeeperReplicationConfig) cfg.replicationConfig();
+ final ZooKeeperReplicationConfigSpec zkCfg = (ZooKeeperReplicationConfigSpec) cfg.replicationConfig();
// Delete the old UUID replica ID which is not used anymore.
final File dataDir = cfg.dataDir();
@@ -810,13 +888,10 @@ private Function super HttpService, AuthService> authService(
if (authProvider == null) {
return AuthService.newDecorator(new CsrfTokenAuthorizer());
}
- final AuthConfig authCfg = cfg.authConfig();
+ final AuthConfigSpec authCfg = cfg.authConfig();
assert authCfg != null : "authCfg";
assert sessionManager != null : "sessionManager";
- final Authorizer tokenAuthorizer =
- new ApplicationTokenAuthorizer(mds::findTokenBySecret)
- .orElse(new SessionTokenAuthorizer(sessionManager,
- authCfg.systemAdministrators()));
+ final Authorizer tokenAuthorizer = tokenAuthorizerCreator.create(mds, sessionManager);
return AuthService.builder()
.add(tokenAuthorizer)
.onFailure(new CentralDogmaAuthFailureHandler())
@@ -895,7 +970,7 @@ protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
}
});
- final AuthConfig authCfg = cfg.authConfig();
+ final AuthConfigSpec authCfg = cfg.authConfig();
assert authCfg != null : "authCfg";
apiV1ServiceBuilder
.annotatedService(new MetadataApiService(executor, mds, authCfg.loginNameNormalizer()))
@@ -955,7 +1030,7 @@ protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
sb.errorHandler(new HttpApiExceptionHandler());
}
- private static void configCors(ServerBuilder sb, @Nullable CorsConfig corsConfig) {
+ private static void configCors(ServerBuilder sb, @Nullable CorsConfigSpec corsConfig) {
if (corsConfig == null) {
return;
}
@@ -968,7 +1043,7 @@ private static void configCors(ServerBuilder sb, @Nullable CorsConfig corsConfig
.newDecorator());
}
- private static void configManagement(ServerBuilder sb, @Nullable ManagementConfig managementConfig) {
+ private static void configManagement(ServerBuilder sb, @Nullable ManagementConfigSpec managementConfig) {
if (managementConfig == null) {
return;
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaBuilder.java b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaBuilder.java
index 1e35ced3c..08ceab165 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaBuilder.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * 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
@@ -17,9 +17,6 @@
package com.linecorp.centraldogma.server;
import static com.google.common.base.Preconditions.checkArgument;
-import static com.linecorp.centraldogma.server.auth.AuthConfig.DEFAULT_SESSION_CACHE_SPEC;
-import static com.linecorp.centraldogma.server.auth.AuthConfig.DEFAULT_SESSION_TIMEOUT_MILLIS;
-import static com.linecorp.centraldogma.server.auth.AuthConfig.DEFAULT_SESSION_VALIDATION_SCHEDULE;
import static com.linecorp.centraldogma.server.internal.storage.repository.RepositoryCache.validateCacheSpec;
import static java.util.Objects.requireNonNull;
@@ -46,6 +43,7 @@
import com.linecorp.armeria.server.ServerPort;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.server.auth.AuthConfig;
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec;
import com.linecorp.centraldogma.server.auth.AuthProvider;
import com.linecorp.centraldogma.server.auth.AuthProviderFactory;
import com.linecorp.centraldogma.server.auth.Session;
@@ -120,9 +118,9 @@ public final class CentralDogmaBuilder {
private AuthProviderFactory authProviderFactory;
private final ImmutableSet.Builder systemAdministrators = new Builder<>();
private boolean caseSensitiveLoginNames;
- private String sessionCacheSpec = DEFAULT_SESSION_CACHE_SPEC;
- private long sessionTimeoutMillis = DEFAULT_SESSION_TIMEOUT_MILLIS;
- private String sessionValidationSchedule = DEFAULT_SESSION_VALIDATION_SCHEDULE;
+ private String sessionCacheSpec = AuthConfigSpec.DEFAULT_SESSION_CACHE_SPEC;
+ private long sessionTimeoutMillis = AuthConfigSpec.DEFAULT_SESSION_TIMEOUT_MILLIS;
+ private String sessionValidationSchedule = AuthConfigSpec.DEFAULT_SESSION_VALIDATION_SCHEDULE;
@Nullable
private Object authProviderProperties;
private int writeQuota;
@@ -446,7 +444,7 @@ public CentralDogmaBuilder caseSensitiveLoginNames(boolean caseSensitiveLoginNam
/**
* Sets the cache specification which determines the capacity and behavior of the cache for
* {@link Session} of the server. See {@link CaffeineSpec} for the syntax of the spec.
- * If unspecified, the default cache spec of {@value AuthConfig#DEFAULT_SESSION_CACHE_SPEC}
+ * If unspecified, the default cache spec of {@value AuthConfigSpec#DEFAULT_SESSION_CACHE_SPEC}
* is used.
*/
public CentralDogmaBuilder sessionCacheSpec(String sessionCacheSpec) {
@@ -456,7 +454,7 @@ public CentralDogmaBuilder sessionCacheSpec(String sessionCacheSpec) {
/**
* Sets the session timeout for administrative web application, in milliseconds.
- * If unspecified, {@value AuthConfig#DEFAULT_SESSION_TIMEOUT_MILLIS} is used.
+ * If unspecified, {@value AuthConfigSpec#DEFAULT_SESSION_TIMEOUT_MILLIS} is used.
*/
public CentralDogmaBuilder sessionTimeoutMillis(long sessionTimeoutMillis) {
this.sessionTimeoutMillis = sessionTimeoutMillis;
@@ -465,7 +463,7 @@ public CentralDogmaBuilder sessionTimeoutMillis(long sessionTimeoutMillis) {
/**
* Sets the session timeout for administrative web application.
- * If unspecified, {@value AuthConfig#DEFAULT_SESSION_TIMEOUT_MILLIS} is used.
+ * If unspecified, {@value AuthConfigSpec#DEFAULT_SESSION_TIMEOUT_MILLIS} is used.
*/
public CentralDogmaBuilder sessionTimeout(Duration sessionTimeout) {
return sessionTimeoutMillis(
@@ -474,7 +472,7 @@ public CentralDogmaBuilder sessionTimeout(Duration sessionTimeout) {
/**
* Sets a schedule for validating sessions.
- * If unspecified, {@value AuthConfig#DEFAULT_SESSION_VALIDATION_SCHEDULE} is used.
+ * If unspecified, {@value AuthConfigSpec#DEFAULT_SESSION_VALIDATION_SCHEDULE} is used.
*/
public CentralDogmaBuilder sessionValidationSchedule(String sessionValidationSchedule) {
this.sessionValidationSchedule =
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaConfig.java
index 0b3894d45..367ddc97b 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaConfig.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * 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
@@ -18,12 +18,6 @@
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;
@@ -41,7 +35,6 @@
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;
@@ -69,28 +62,25 @@
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.auth.AuthConfigSpec;
import com.linecorp.centraldogma.server.plugin.PluginConfig;
import com.linecorp.centraldogma.server.plugin.PluginConfigDeserializer;
-import com.linecorp.centraldogma.server.plugin.PluginTarget;
-import com.linecorp.centraldogma.server.storage.repository.Repository;
import io.netty.util.NetUtil;
/**
* {@link CentralDogma} server configuration.
*/
-public final class CentralDogmaConfig {
+public final class CentralDogmaConfig extends AbstractCentralDogmaConfig {
private static final Logger logger = LoggerFactory.getLogger(CentralDogmaConfig.class);
@@ -265,7 +255,6 @@ public static CentralDogmaConfig load(String json) throws JsonMappingException,
private final CorsConfig corsConfig;
private final List pluginConfigs;
- private final Map, PluginConfig> pluginConfigMap;
@Nullable
private final ManagementConfig managementConfig;
@@ -302,6 +291,8 @@ public static CentralDogmaConfig load(String json) throws JsonMappingException,
@JsonProperty("management") @Nullable ManagementConfig managementConfig,
@JsonProperty("zone") @Nullable ZoneConfig zoneConfig) {
+ super(pluginConfigs);
+
this.dataDir = requireNonNull(dataDir, "dataDir");
this.ports = ImmutableList.copyOf(requireNonNull(ports, "ports"));
checkArgument(!ports.isEmpty(), "ports must have at least one port.");
@@ -346,130 +337,94 @@ public static CentralDogmaConfig load(String json) throws JsonMappingException,
this.writeQuotaPerRepository = writeQuotaPerRepository;
this.corsConfig = corsConfig;
this.pluginConfigs = firstNonNull(pluginConfigs, ImmutableList.of());
- pluginConfigMap = this.pluginConfigs.stream().collect(
- toImmutableMap(PluginConfig::getClass, Function.identity()));
this.managementConfig = managementConfig;
this.zoneConfig = zoneConfig;
}
- /**
- * Returns the data directory.
- */
@JsonProperty
+ @Override
public File dataDir() {
return dataDir;
}
- /**
- * Returns the {@link ServerPort}s.
- */
@JsonProperty
@JsonSerialize(contentUsing = ServerPortSerializer.class)
+ @Override
public List ports() {
return ports;
}
- /**
- * Returns the TLS configuration.
- */
@Nullable
@JsonProperty
- public TlsConfig tls() {
+ @Override
+ public TlsConfigSpec 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
+ @Override
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
+ @Override
public List clientAddressSources() {
return clientAddressSources;
}
- /**
- * Returns the number of event loop threads.
- */
@JsonProperty
@JsonSerialize(converter = OptionalConverter.class)
+ @Override
public Optional numWorkers() {
return Optional.ofNullable(numWorkers);
}
- /**
- * Returns the maximum number of established connections.
- */
@JsonProperty
@JsonSerialize(converter = OptionalConverter.class)
+ @Override
public Optional maxNumConnections() {
return Optional.ofNullable(maxNumConnections);
}
- /**
- * Returns the request timeout in milliseconds.
- */
@JsonProperty
@JsonSerialize(converter = OptionalConverter.class)
+ @Override
public Optional requestTimeoutMillis() {
return Optional.ofNullable(requestTimeoutMillis);
}
- /**
- * Returns the timeout of an idle connection in milliseconds.
- */
@JsonProperty
@JsonSerialize(converter = OptionalConverter.class)
+ @Override
public Optional idleTimeoutMillis() {
return Optional.ofNullable(idleTimeoutMillis);
}
- /**
- * Returns the maximum length of request content in bytes.
- */
@JsonProperty
@JsonSerialize(converter = OptionalConverter.class)
+ @Override
public Optional maxFrameLength() {
return Optional.ofNullable(maxFrameLength);
}
- /**
- * Returns the number of repository worker threads.
- */
@JsonProperty
- int numRepositoryWorkers() {
+ @Override
+ public 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
+ @Override
public long maxRemovedRepositoryAgeMillis() {
return maxRemovedRepositoryAgeMillis;
}
/**
- * Returns the {@code repositoryCacheSpec}.
- *
- * @deprecated Use {@link #repositoryCacheSpec()}.
+ * Replaced to {@link CentralDogmaConfig#repositoryCacheSpec() }.
+ * @deprecated This cache spec is replaced to {@link CentralDogmaConfig#repositoryCacheSpec() }.
*/
@JsonProperty
@Deprecated
@@ -477,123 +432,88 @@ public String cacheSpec() {
return repositoryCacheSpec;
}
- /**
- * Returns the cache spec of the repository cache.
- */
@JsonProperty
+ @Override
public String repositoryCacheSpec() {
return repositoryCacheSpec;
}
- /**
- * Returns the graceful shutdown timeout.
- */
@JsonProperty
@JsonSerialize(converter = OptionalConverter.class)
- public Optional gracefulShutdownTimeout() {
+ @Override
+ public Optional gracefulShutdownTimeout() {
return Optional.ofNullable(gracefulShutdownTimeout);
}
- /**
- * Returns whether web app is enabled.
- */
@JsonProperty
+ @Override
public boolean isWebAppEnabled() {
return webAppEnabled;
}
- /**
- * Returns the title of the web app.
- */
@Nullable
@JsonProperty("webAppTitle")
+ @Override
public String webAppTitle() {
return webAppTitle;
}
- /**
- * Returns the {@link ReplicationConfig}.
- */
@JsonProperty("replication")
+ @Override
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
+ @Override
public boolean isCsrfTokenRequiredForThrift() {
return csrfTokenRequiredForThrift;
}
- /**
- * Returns the access log format.
- */
@JsonProperty
@Nullable
+ @Override
public String accessLogFormat() {
return accessLogFormat;
}
- /**
- * Returns the {@link AuthConfig}.
- */
@Nullable
@JsonProperty("authentication")
- public AuthConfig authConfig() {
+ @Override
+ public AuthConfigSpec authConfig() {
return authConfig;
}
- /**
- * Returns the maximum allowed write quota per {@link Repository}.
- */
@Nullable
@JsonProperty("writeQuotaPerRepository")
- public QuotaConfig writeQuotaPerRepository() {
+ @Override
+ public QuotaConfigSpec writeQuotaPerRepository() {
return writeQuotaPerRepository;
}
- /**
- * Returns the {@link CorsConfig}.
- */
@Nullable
@JsonProperty("cors")
- public CorsConfig corsConfig() {
+ @Override
+ public CorsConfigSpec corsConfig() {
return corsConfig;
}
- /**
- * Returns the list of {@link PluginConfig}s.
- */
@JsonProperty("pluginConfigs")
+ @Override
public List pluginConfigs() {
return pluginConfigs;
}
- /**
- * Returns the map of {@link PluginConfig}s.
- */
- public Map, PluginConfig> pluginConfigMap() {
- return pluginConfigMap;
- }
-
- /**
- * Returns the {@link ManagementConfig}.
- */
@Nullable
@JsonProperty("management")
- public ManagementConfig managementConfig() {
+ @Override
+ public ManagementConfigSpec managementConfig() {
return managementConfig;
}
- /**
- * Returns the zone information of the server.
- * Note that the zone must be specified to use the {@link PluginTarget#ZONE_LEADER_ONLY} target.
- */
@Nullable
@JsonProperty("zone")
+ @Override
public ZoneConfig zone() {
return zoneConfig;
}
@@ -607,46 +527,16 @@ public String toString() {
}
}
- Predicate trustedProxyAddressPredicate() {
+ @Override
+ public Predicate trustedProxyAddressPredicate() {
return trustedProxyAddressPredicate;
}
- List clientAddressSourceList() {
+ @Override
+ public 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,
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaConfigSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaConfigSpec.java
new file mode 100644
index 000000000..b5acf2681
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/CentralDogmaConfigSpec.java
@@ -0,0 +1,210 @@
+/*
+ * 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 java.io.File;
+import java.net.InetAddress;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import javax.annotation.Nullable;
+
+import com.linecorp.armeria.server.ClientAddressSource;
+import com.linecorp.armeria.server.ServerPort;
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec;
+import com.linecorp.centraldogma.server.plugin.PluginConfig;
+import com.linecorp.centraldogma.server.plugin.PluginTarget;
+import com.linecorp.centraldogma.server.storage.repository.Repository;
+
+/**
+ * {@link CentralDogma} server configuration spec.
+ */
+public interface CentralDogmaConfigSpec {
+ /**
+ * Returns the data directory.
+ */
+ File dataDir();
+
+ /**
+ * Returns the {@link ServerPort}s.
+ */
+ List ports();
+
+ /**
+ * Returns the TLS configuration.
+ */
+ @Nullable
+ TlsConfigSpec 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
+ List trustedProxyAddresses();
+
+ /**
+ * Returns the sources that determines a client address. For example:
+ *
+ *
{@code "forwarded"}
+ *
{@code "x-forwarded-for"}
+ *
{@code "PROXY_PROTOCOL"}
+ *
+ *
+ */
+ @Nullable
+ List clientAddressSources();
+
+ /**
+ * Returns the number of event loop threads.
+ */
+ Optional numWorkers();
+
+ /**
+ * Returns the maximum number of established connections.
+ */
+ Optional maxNumConnections();
+
+ /**
+ * Returns the request timeout in milliseconds.
+ */
+ Optional requestTimeoutMillis();
+
+ /**
+ * Returns the timeout of an idle connection in milliseconds.
+ */
+ Optional idleTimeoutMillis();
+
+ /**
+ * Returns the maximum length of request content in bytes.
+ */
+ Optional maxFrameLength();
+
+ /**
+ * Returns the number of repository worker threads.
+ */
+ int 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.
+ */
+ long maxRemovedRepositoryAgeMillis();
+
+ /**
+ * Returns the cache spec of the repository cache.
+ */
+ String repositoryCacheSpec();
+
+ /**
+ * Returns the graceful shutdown timeout.
+ */
+ Optional gracefulShutdownTimeout();
+
+ /**
+ * Returns whether web app is enabled.
+ */
+
+ boolean isWebAppEnabled();
+
+ /**
+ * Returns the title of the web app.
+ */
+ @Nullable
+ String webAppTitle();
+
+ /**
+ * Returns the {@link ReplicationConfig}.
+ */
+
+ ReplicationConfig 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.
+ */
+
+ boolean isCsrfTokenRequiredForThrift();
+
+ /**
+ * Returns the access log format.
+ */
+ @Nullable
+ String accessLogFormat();
+
+ /**
+ * Returns the {@link AuthConfigSpec}.
+ */
+ @Nullable
+ AuthConfigSpec authConfig();
+
+ /**
+ * Returns the maximum allowed write quota per {@link Repository}.
+ */
+ @Nullable
+ QuotaConfigSpec writeQuotaPerRepository();
+
+ /**
+ * Returns the {@link CorsConfigSpec}.
+ */
+ @Nullable
+ CorsConfigSpec corsConfig();
+
+ /**
+ * Returns the list of {@link PluginConfig}s.
+ */
+ List pluginConfigs();
+
+ /**
+ * Returns the map of {@link PluginConfig}s.
+ * @deprecated This will be removed soon.
+ */
+ @Deprecated
+ Map, PluginConfig> pluginConfigMap();
+
+ /**
+ * Returns the {@link PluginConfig} with casting.
+ */
+ @Nullable
+ T pluginConfig(Class pluginClass);
+
+ /**
+ * Returns the {@link ManagementConfig}.
+ */
+ @Nullable
+ ManagementConfigSpec managementConfig();
+
+ /**
+ * Returns the zone information of the server.
+ * Note that the zone must be specified to use the {@link PluginTarget#ZONE_LEADER_ONLY} target.
+ */
+ @Nullable
+ ZoneConfigSpec zone();
+
+ /**
+ * Returns the predicate of trusted proxy address.
+ */
+ Predicate trustedProxyAddressPredicate();
+
+ /**
+ * Returns the client address source list.
+ */
+ List clientAddressSourceList();
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/CorsConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/CorsConfig.java
index aec3d4a8a..f793aa2d8 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/CorsConfig.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/CorsConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 LINE Corporation
+ * 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
@@ -28,9 +28,7 @@
/**
* CORS configuration.
*/
-public final class CorsConfig {
-
- private static final int DEFAULT_MAX_AGE = 7200;
+public final class CorsConfig implements CorsConfigSpec {
private final List allowedOrigins;
private final int maxAgeSeconds;
@@ -60,7 +58,7 @@ public CorsConfig(@JsonProperty("allowedOrigins") Object allowedOrigins,
}
if (maxAgeSeconds == null) {
- maxAgeSeconds = DEFAULT_MAX_AGE;
+ maxAgeSeconds = CorsConfigSpec.DEFAULT_MAX_AGE;
}
if (maxAgeSeconds <= 0) {
throw new IllegalArgumentException(
@@ -70,19 +68,14 @@ public CorsConfig(@JsonProperty("allowedOrigins") Object allowedOrigins,
this.maxAgeSeconds = maxAgeSeconds;
}
- /**
- * Returns the list of origins which are allowed a CORS policy.
- */
@JsonProperty
+ @Override
public List allowedOrigins() {
return allowedOrigins;
}
- /**
- * Returns how long in seconds the results of a preflight request can be cached.
- * If unspecified, the default of {@code 7200} seconds is returned.
- */
@JsonProperty
+ @Override
public int maxAgeSeconds() {
return maxAgeSeconds;
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/CorsConfigSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/CorsConfigSpec.java
new file mode 100644
index 000000000..5623afc15
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/CorsConfigSpec.java
@@ -0,0 +1,41 @@
+/*
+ * 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 java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * CORS configuration spec.
+ */
+public interface CorsConfigSpec {
+ int DEFAULT_MAX_AGE = 7200;
+
+ /**
+ * Returns the list of origins which are allowed a CORS policy.
+ */
+ @JsonProperty
+ List allowedOrigins();
+
+ /**
+ * Returns how long in seconds the results of a preflight request can be cached.
+ * If unspecified, the default of {@code 7200} seconds is returned.
+ */
+ @JsonProperty
+ int maxAgeSeconds();
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/GracefulShutdownTimeout.java b/server/src/main/java/com/linecorp/centraldogma/server/GracefulShutdownTimeout.java
index 8bafd8626..470ca8af4 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/GracefulShutdownTimeout.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/GracefulShutdownTimeout.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * 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
@@ -23,7 +23,7 @@
/**
* Graceful shutdown timeout.
*/
-public final class GracefulShutdownTimeout {
+public final class GracefulShutdownTimeout implements GracefulShutdownTimeoutSpec {
private final long quietPeriodMillis;
private final long timeoutMillis;
@@ -47,18 +47,14 @@ public GracefulShutdownTimeout(
this.timeoutMillis = timeoutMillis;
}
- /**
- * Returns the quiet period of graceful shutdown process, in milliseconds.
- */
@JsonProperty("quietPeriodMillis")
+ @Override
public long quietPeriodMillis() {
return quietPeriodMillis;
}
- /**
- * Returns the timeout of graceful shutdown process, in milliseconds.
- */
@JsonProperty("timeoutMillis")
+ @Override
public long timeoutMillis() {
return timeoutMillis;
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/GracefulShutdownTimeoutSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/GracefulShutdownTimeoutSpec.java
new file mode 100644
index 000000000..ce3421e42
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/GracefulShutdownTimeoutSpec.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Graceful shutdown timeout spec.
+ */
+public interface GracefulShutdownTimeoutSpec {
+ /**
+ * Returns the quiet period of graceful shutdown process, in milliseconds.
+ */
+ long quietPeriodMillis();
+
+ /**
+ * Returns the timeout of graceful shutdown process, in milliseconds.
+ */
+ long timeoutMillis();
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/ManagementConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/ManagementConfig.java
index 96c7da8b9..2e34ede61 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/ManagementConfig.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/ManagementConfig.java
@@ -32,9 +32,7 @@
/**
* 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";
+public final class ManagementConfig implements ManagementConfigSpec {
private final SessionProtocol protocol;
private final @Nullable String address;
@@ -70,34 +68,26 @@ public ManagementConfig(@Nullable SessionProtocol protocol,
this.path = firstNonNull(path, DEFAULT_PATH);
}
- /**
- * Returns the protocol of the management service.
- */
@JsonProperty("protocol")
+ @Override
public SessionProtocol protocol() {
return protocol;
}
- /**
- * Returns the address of the management service.
- */
@JsonProperty("address")
+ @Override
public @Nullable String address() {
return address;
}
- /**
- * Returns the port of the management service.
- */
@JsonProperty("port")
+ @Override
public int port() {
return port;
}
- /**
- * Returns the path of the management service.
- */
@JsonProperty("path")
+ @Override
public String path() {
return path;
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/ManagementConfigSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/ManagementConfigSpec.java
new file mode 100644
index 000000000..a92d3e849
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/ManagementConfigSpec.java
@@ -0,0 +1,50 @@
+/*
+ * 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 com.linecorp.armeria.common.SessionProtocol;
+import com.linecorp.armeria.common.annotation.Nullable;
+import com.linecorp.armeria.server.management.ManagementService;
+
+/**
+ * A configuration spec for the {@link ManagementService}.
+ */
+public interface ManagementConfigSpec {
+ String DEFAULT_PROTOCOL = "http";
+ String DEFAULT_PATH = "/internal/management";
+
+ /**
+ * Returns the protocol of the management service.
+ */
+ SessionProtocol protocol();
+
+ /**
+ * Returns the address of the management service.
+ */
+ @Nullable
+ String address();
+
+ /**
+ * Returns the port of the management service.
+ */
+ int port();
+
+ /**
+ * Returns the path of the management service.
+ */
+ String path();
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/PluginGroup.java b/server/src/main/java/com/linecorp/centraldogma/server/PluginGroup.java
index a68c12d0d..9b5f8dff4 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/PluginGroup.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/PluginGroup.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 LINE Corporation
+ * 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
@@ -30,6 +30,7 @@
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
@@ -69,7 +70,7 @@ final class PluginGroup {
*/
@VisibleForTesting
@Nullable
- static PluginGroup loadPlugins(PluginTarget target, CentralDogmaConfig config) {
+ static PluginGroup loadPlugins(PluginTarget target, CentralDogmaConfigSpec config) {
return loadPlugins(PluginGroup.class.getClassLoader(), config, ImmutableList.of()).get(target);
}
@@ -80,27 +81,33 @@ static PluginGroup loadPlugins(PluginTarget target, CentralDogmaConfig config) {
*
* @param classLoader which is used to load the {@link Plugin}s
*/
- static Map loadPlugins(ClassLoader classLoader, CentralDogmaConfig config,
+ static Map loadPlugins(ClassLoader classLoader, CentralDogmaConfigSpec config,
List plugins) {
requireNonNull(classLoader, "classLoader");
requireNonNull(config, "config");
final ServiceLoader loader = ServiceLoader.load(Plugin.class, classLoader);
- final ImmutableMap.Builder, Plugin> allPlugins = new ImmutableMap.Builder<>();
- for (Plugin plugin : Iterables.concat(plugins, loader)) {
- if (plugin.isEnabled(config)) {
- allPlugins.put(plugin.configType(), plugin);
- }
+
+ final List allPlugins = StreamSupport.stream(Iterables.concat(plugins, loader).spliterator(),
+ false)
+ .filter(plugin -> plugin.isEnabled(config))
+ .collect(toImmutableList());
+
+ final long uniquePluginCounts = allPlugins.stream()
+ .map(Plugin::getClass)
+ .distinct()
+ .count();
+
+ if (allPlugins.size() != uniquePluginCounts) {
+ throw new IllegalArgumentException("Found duplicated plugins");
}
- // IllegalArgumentException is thrown if there are duplicate keys.
- final Map, Plugin> pluginMap = allPlugins.build();
- if (pluginMap.isEmpty()) {
+ if (allPlugins.isEmpty()) {
return ImmutableMap.of();
}
final Map pluginGroups =
- pluginMap.values()
+ allPlugins
.stream()
.collect(Collectors.groupingBy(plugin -> plugin.target(config)))
.entrySet()
@@ -150,7 +157,7 @@ T findFirstPlugin(Class clazz) {
/**
* Starts the {@link Plugin}s managed by this {@link PluginGroup}.
*/
- CompletableFuture start(CentralDogmaConfig config, ProjectManager projectManager,
+ CompletableFuture start(CentralDogmaConfigSpec config, ProjectManager projectManager,
CommandExecutor commandExecutor, MeterRegistry meterRegistry,
ScheduledExecutorService purgeWorker,
InternalProjectInitializer internalProjectInitializer) {
@@ -162,7 +169,7 @@ CompletableFuture start(CentralDogmaConfig config, ProjectManager projectM
/**
* Stops the {@link Plugin}s managed by this {@link PluginGroup}.
*/
- CompletableFuture stop(CentralDogmaConfig config, ProjectManager projectManager,
+ CompletableFuture stop(CentralDogmaConfigSpec config, ProjectManager projectManager,
CommandExecutor commandExecutor, MeterRegistry meterRegistry,
ScheduledExecutorService purgeWorker,
InternalProjectInitializer internalProjectInitializer) {
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/QuotaConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/QuotaConfig.java
index 5f9cb8c13..2da7d130a 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/QuotaConfig.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/QuotaConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 LINE Corporation
+ * 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
@@ -24,7 +24,7 @@
/**
* {@link CentralDogma} API quota configuration.
*/
-public final class QuotaConfig {
+public final class QuotaConfig implements QuotaConfigSpec {
private final int requestQuota;
private final int timeWindowSeconds;
@@ -42,29 +42,18 @@ public QuotaConfig(@JsonProperty("requestQuota") int requestQuota,
this.timeWindowSeconds = timeWindowSeconds;
}
- /**
- * Returns a time windows in seconds which is created with the given {@code timeWindowSeconds}.
- */
@JsonProperty
+ @Override
public int timeWindowSeconds() {
return timeWindowSeconds;
}
- /**
- * Returns a maximum number of acceptable requests which is created with the given {@code requestQuota}.
- */
@JsonProperty
+ @Override
public int requestQuota() {
return requestQuota;
}
- /**
- * Returns a maximum number of acceptable requests per second.
- */
- public double permitsPerSecond() {
- return requestQuota() * 1.0 / timeWindowSeconds();
- }
-
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/QuotaConfigSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/QuotaConfigSpec.java
new file mode 100644
index 000000000..16b66d4f3
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/QuotaConfigSpec.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * {@link CentralDogma} API quota configuration spec.
+ */
+public interface QuotaConfigSpec {
+ /**
+ * Returns a time windows in seconds which is created with the given {@code timeWindowSeconds}.
+ */
+ int timeWindowSeconds();
+
+ /**
+ * Returns a maximum number of acceptable requests which is created with the given {@code requestQuota}.
+ */
+ int requestQuota();
+
+ /**
+ * Returns a maximum number of acceptable requests per second.
+ */
+ default double permitsPerSecond() {
+ return requestQuota() * 1.0 / timeWindowSeconds();
+ }
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/TlsConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/TlsConfig.java
index a4ea2af80..241820dcf 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/TlsConfig.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/TlsConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * 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
@@ -35,7 +35,7 @@
/**
* TLS configuration.
*/
-public final class TlsConfig {
+public final class TlsConfig implements TlsConfigSpec {
@Nullable
private final File keyCertChainFile;
@@ -103,18 +103,14 @@ public File keyFile() {
return keyFile;
}
- /**
- * Returns an {@link InputStream} of the certificate chain.
- */
@MustBeClosed
+ @Override
public InputStream keyCertChainInputStream() {
return inputStream(keyCertChainFile, keyCertChain, "keyCertChain");
}
- /**
- * Returns an {@link InputStream} of the private key.
- */
@MustBeClosed
+ @Override
public InputStream keyInputStream() {
return inputStream(keyFile, key, "key");
}
@@ -138,11 +134,9 @@ private static InputStream inputStream(@Nullable File file,
return new ByteArrayInputStream(converted.getBytes());
}
- /**
- * Returns a password for the private key file. Return {@code null} if no password is set.
- */
@JsonProperty
@Nullable
+ @Override
public String keyPassword() {
return convertValue(keyPassword, "tls.keyPassword");
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/TlsConfigSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/TlsConfigSpec.java
new file mode 100644
index 000000000..4bcbcc199
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/TlsConfigSpec.java
@@ -0,0 +1,46 @@
+/*
+ * 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 java.io.InputStream;
+
+import javax.annotation.Nullable;
+
+import com.google.errorprone.annotations.MustBeClosed;
+
+/**
+ * TLS configuration spec.
+ */
+public interface TlsConfigSpec {
+ /**
+ * Returns an {@link InputStream} of the certificate chain.
+ */
+ @MustBeClosed
+ InputStream keyCertChainInputStream();
+
+ /**
+ * Returns an {@link InputStream} of the private key.
+ */
+ @MustBeClosed
+ InputStream keyInputStream();
+
+ /**
+ * Returns a password for the private key file. Return {@code null} if no password is set.
+ */
+ @Nullable
+ String keyPassword();
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/ZoneConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/ZoneConfig.java
index 96bde8aca..00d9b015d 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/ZoneConfig.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/ZoneConfig.java
@@ -31,7 +31,7 @@
/**
* A configuration class for the zone.
*/
-public final class ZoneConfig {
+public final class ZoneConfig implements ZoneConfigSpec {
private final String currentZone;
private final List allZones;
@@ -50,18 +50,14 @@ public ZoneConfig(@JsonProperty("currentZone") String currentZone,
currentZone, allZones);
}
- /**
- * Returns the current zone.
- */
@JsonProperty("currentZone")
+ @Override
public String currentZone() {
return currentZone;
}
- /**
- * Returns all zones.
- */
@JsonProperty("allZones")
+ @Override
public List allZones() {
return allZones;
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/ZoneConfigSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/ZoneConfigSpec.java
new file mode 100644
index 000000000..9facccf48
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/ZoneConfigSpec.java
@@ -0,0 +1,34 @@
+/*
+ * 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 java.util.List;
+
+/**
+ * A configuration spec for the zone.
+ */
+public interface ZoneConfigSpec {
+ /**
+ * Returns the current zone.
+ */
+ String currentZone();
+
+ /**
+ * Returns all zones.
+ */
+ List allZones();
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfig.java
index 067077963..1600a631a 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfig.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * 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
@@ -21,14 +21,7 @@
import static com.linecorp.centraldogma.server.CentralDogmaConfig.convertValue;
import static java.util.Objects.requireNonNull;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.util.Enumeration;
import java.util.Map;
-import java.util.Map.Entry;
-import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
@@ -39,21 +32,13 @@
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
-import io.netty.util.NetUtil;
-
/**
* ZooKeeper-based replication configuration.
*/
-public final class ZooKeeperReplicationConfig implements ReplicationConfig {
-
- private static final int DEFAULT_TIMEOUT_MILLIS = 10000;
- private static final int DEFAULT_NUM_WORKERS = 16;
- private static final int DEFAULT_MAX_LOG_COUNT = 1024;
- private static final long DEFAULT_MIN_LOG_AGE_MILLIS = TimeUnit.DAYS.toMillis(1);
- private static final String DEFAULT_SECRET = "ch4n63m3";
+public final class ZooKeeperReplicationConfig implements ZooKeeperReplicationConfigSpec {
private final int serverId;
- private final Map servers;
+ private final Map servers;
@Nullable
private final String secret;
private final Map additionalProperties;
@@ -68,13 +53,13 @@ public final class ZooKeeperReplicationConfig implements ReplicationConfig {
* @param serverId the ID of this ZooKeeper server in {@code servers}
* @param servers the ZooKeeper server addresses, keyed by their ZooKeeper server IDs
*/
- public ZooKeeperReplicationConfig(int serverId, Map servers) {
+ public ZooKeeperReplicationConfig(int serverId, Map servers) {
this(serverId, servers, null, null, null, null, null, null);
}
@VisibleForTesting
ZooKeeperReplicationConfig(
- int serverId, Map servers, String secret,
+ int serverId, Map servers, String secret,
Map additionalProperties,
int timeoutMillis, int numWorkers, int maxLogCount, long minLogAgeMillis) {
this(Integer.valueOf(serverId), servers, secret, additionalProperties, Integer.valueOf(timeoutMillis),
@@ -85,7 +70,7 @@ public ZooKeeperReplicationConfig(int serverId, Map servers,
+ Map servers,
@JsonProperty("secret") @Nullable String secret,
@JsonProperty("additionalProperties")
@JsonDeserialize(keyAs = String.class, contentAs = String.class)
@@ -96,7 +81,7 @@ public ZooKeeperReplicationConfig(int serverId, Map 0, "serverId: %s (expected: > 0)", serverId);
this.secret = secret;
checkArgument(!secret().isEmpty(), "secret is empty.");
@@ -125,137 +110,59 @@ public ZooKeeperReplicationConfig(int serverId, Map servers) {
- int serverId = -1;
- try {
- for (final Enumeration e = NetworkInterface.getNetworkInterfaces();
- e.hasMoreElements();) {
- serverId = findServerId(servers, serverId, e.nextElement());
- }
- } catch (SocketException e) {
- throw new IllegalStateException("failed to retrieve the network interface list", e);
- }
-
- if (serverId < 0) {
- throw new IllegalStateException(
- "failed to auto-detect server ID because there is no matching IP address.");
- }
-
- return serverId;
- }
-
- private static int findServerId(Map servers, int currentServerId,
- NetworkInterface iface) {
- for (final Enumeration ea = iface.getInetAddresses(); ea.hasMoreElements();) {
- currentServerId = findServerId(servers, currentServerId, ea.nextElement());
- }
- return currentServerId;
- }
-
- private static int findServerId(Map servers, int currentServerId,
- InetAddress addr) {
- final String ip = NetUtil.toAddressString(addr, true);
- for (Entry entry : servers.entrySet()) {
- final String zkAddr;
- try {
- zkAddr = NetUtil.toAddressString(InetAddress.getByName(entry.getValue().host()), true);
- } catch (UnknownHostException uhe) {
- throw new IllegalStateException(
- "failed to resolve the IP address of the server name: " + entry.getValue().host());
- }
-
- if (zkAddr.equals(ip)) {
- final int serverId = entry.getKey().intValue();
- if (currentServerId < 0) {
- currentServerId = serverId;
- } else if (currentServerId != serverId) {
- throw new IllegalStateException(
- "cannot auto-detect server ID because there are more than one IP address match. " +
- "Both server ID " + currentServerId + " and " + serverId +
- " have a matching IP address. Consider specifying server ID explicitly.");
- }
- }
- }
- return currentServerId;
- }
-
@Override
public ReplicationMethod method() {
return ReplicationMethod.ZOOKEEPER;
}
- /**
- * Returns the ID of this ZooKeeper server in {@link #servers()}.
- */
@JsonProperty
+ @Override
public int serverId() {
return serverId;
}
- /**
- * Returns the configuration of this ZooKeeper server in {@link #servers()}.
- */
- public ZooKeeperServerConfig serverConfig() {
+ @Override
+ public ZooKeeperServerConfigSpec serverConfig() {
return servers.get(serverId);
}
- /**
- * Returns the configuration of all ZooKeeper servers, keyed by their server IDs.
- */
@JsonProperty
- public Map servers() {
+ @Override
+ public Map servers() {
return servers;
}
- /**
- * Returns the secret string used for authenticating the ZooKeeper peers.
- */
+ @Override
public String secret() {
return firstNonNull(convertValue(secret, "replication.secret"), DEFAULT_SECRET);
}
- /**
- * Returns the additional ZooKeeper properties.
- * If unspecified, an empty {@link Map} is returned.
- */
@JsonProperty
+ @Override
public Map additionalProperties() {
return additionalProperties;
}
- /**
- * Returns the ZooKeeper timeout, in milliseconds.
- * If unspecified, the default of {@value #DEFAULT_TIMEOUT_MILLIS} is returned.
- */
@JsonProperty
+ @Override
public int timeoutMillis() {
return timeoutMillis;
}
- /**
- * Returns the number of worker threads dedicated for replication.
- * If unspecified, the default of {@value #DEFAULT_NUM_WORKERS} is returned.
- */
@JsonProperty
+ @Override
public int numWorkers() {
return numWorkers;
}
- /**
- * Returns the maximum number of log items to keep in ZooKeeper. Note that the log items will still not be
- * removed if they are younger than {@link #minLogAgeMillis()}.
- * If unspecified, the default of {@value #DEFAULT_MAX_LOG_COUNT} is returned.
- */
@JsonProperty
+ @Override
public int maxLogCount() {
return maxLogCount;
}
- /**
- * Returns the minimum allowed age of log items before they are removed from ZooKeeper.
- * If unspecified, the default of 1 hour is returned.
- */
@JsonProperty
+ @Override
public long minLogAgeMillis() {
return minLogAgeMillis;
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfigSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfigSpec.java
new file mode 100644
index 000000000..4e27fe8ea
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperReplicationConfigSpec.java
@@ -0,0 +1,162 @@
+/*
+ * 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 java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import io.netty.util.NetUtil;
+
+/**
+ * ZooKeeper-based replication configuration spec.
+ */
+public interface ZooKeeperReplicationConfigSpec extends ReplicationConfig {
+ int DEFAULT_TIMEOUT_MILLIS = 10000;
+ int DEFAULT_NUM_WORKERS = 16;
+ int DEFAULT_MAX_LOG_COUNT = 1024;
+ long DEFAULT_MIN_LOG_AGE_MILLIS = TimeUnit.DAYS.toMillis(1);
+ String DEFAULT_SECRET = "ch4n63m3";
+
+ /**
+ * Find a server id from given zooKeeper server configs.
+ * @param servers zooKeeper server configs.
+ * @return serverId
+ */
+ static int findServerId(Map servers) {
+ int serverId = -1;
+ try {
+ for (final Enumeration e = NetworkInterface.getNetworkInterfaces();
+ e.hasMoreElements();) {
+ serverId = findServerId(servers, serverId, e.nextElement());
+ }
+ } catch (SocketException e) {
+ throw new IllegalStateException("failed to retrieve the network interface list", e);
+ }
+
+ if (serverId < 0) {
+ throw new IllegalStateException(
+ "failed to auto-detect server ID because there is no matching IP address.");
+ }
+
+ return serverId;
+ }
+
+ /**
+ * Find a server id from given zooKeeper server configs.
+ * @param servers ZooKeeper server configs.
+ * @param currentServerId Current server id.
+ * @param iface The network interface for current server instance.
+ * @return serverId
+ */
+ static int findServerId(Map servers, int currentServerId,
+ NetworkInterface iface) {
+ for (final Enumeration ea = iface.getInetAddresses(); ea.hasMoreElements();) {
+ currentServerId = findServerId(servers, currentServerId, ea.nextElement());
+ }
+ return currentServerId;
+ }
+
+ /**
+ * Find a server id from given zooKeeper server configs.
+ * @param servers ZooKeeper server configs.
+ * @param currentServerId Current server id.
+ * @param addr The inet address which same as current server instance.
+ * @return serverId
+ */
+ static int findServerId(Map servers, int currentServerId,
+ InetAddress addr) {
+ final String ip = NetUtil.toAddressString(addr, true);
+ for (Map.Entry entry : servers.entrySet()) {
+ final String zkAddr;
+ try {
+ zkAddr = NetUtil.toAddressString(InetAddress.getByName(entry.getValue().host()), true);
+ } catch (UnknownHostException uhe) {
+ throw new IllegalStateException(
+ "failed to resolve the IP address of the server name: " + entry.getValue().host());
+ }
+
+ if (zkAddr.equals(ip)) {
+ final int serverId = entry.getKey();
+ if (currentServerId < 0) {
+ currentServerId = serverId;
+ } else if (currentServerId != serverId) {
+ throw new IllegalStateException(
+ "cannot auto-detect server ID because there are more than one IP address match. " +
+ "Both server ID " + currentServerId + " and " + serverId +
+ " have a matching IP address. Consider specifying server ID explicitly.");
+ }
+ }
+ }
+ return currentServerId;
+ }
+
+ /**
+ * Returns the ID of this ZooKeeper server in {@link #servers()}.
+ */
+ int serverId();
+
+ /**
+ * Returns the configuration of this ZooKeeper server in {@link #servers()}.
+ */
+ ZooKeeperServerConfigSpec serverConfig();
+
+ /**
+ * Returns the configuration of all ZooKeeper servers, keyed by their server IDs.
+ */
+ Map servers();
+
+ /**
+ * Returns the secret string used for authenticating the ZooKeeper peers.
+ */
+ String secret();
+
+ /**
+ * Returns the additional ZooKeeper properties.
+ * If unspecified, an empty {@link java.util.Map} is returned.
+ */
+ Map additionalProperties();
+
+ /**
+ * Returns the ZooKeeper timeout, in milliseconds.
+ * If unspecified, the default of {@value #DEFAULT_TIMEOUT_MILLIS} is returned.
+ */
+ int timeoutMillis();
+
+ /**
+ * Returns the number of worker threads dedicated for replication.
+ * If unspecified, the default of {@value #DEFAULT_NUM_WORKERS} is returned.
+ */
+ int numWorkers();
+
+ /**
+ * Returns the maximum number of log items to keep in ZooKeeper. Note that the log items will still not be
+ * removed if they are younger than {@link #minLogAgeMillis()}.
+ * If unspecified, the default of {@value #DEFAULT_MAX_LOG_COUNT} is returned.
+ */
+ int maxLogCount();
+
+ /**
+ * Returns the minimum allowed age of log items before they are removed from ZooKeeper.
+ * If unspecified, the default of 1 hour is returned.
+ */
+ long minLogAgeMillis();
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperServerConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperServerConfig.java
index a610d0c8c..d38559222 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperServerConfig.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperServerConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * 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
@@ -16,6 +16,7 @@
package com.linecorp.centraldogma.server;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.linecorp.centraldogma.server.ZooKeeperServerConfigSpec.validatePort;
import static java.util.Objects.requireNonNull;
import java.util.Objects;
@@ -29,7 +30,7 @@
/**
* Represents the address and port numbers of a ZooKeeper node.
*/
-public final class ZooKeeperServerConfig {
+public final class ZooKeeperServerConfig implements ZooKeeperServerConfigSpec {
private final String host;
private final int quorumPort;
@@ -66,7 +67,7 @@ public ZooKeeperServerConfig(
clientPort = 0;
}
checkArgument(clientPort >= 0 && clientPort <= 65535,
- "clientPort: %s (expected: 0-65535)", clientPort);
+ "clientPort: %s (expected: 0-65535)", clientPort);
this.clientPort = clientPort;
this.groupId = groupId;
if (weight == null) {
@@ -76,56 +77,39 @@ public ZooKeeperServerConfig(
this.weight = weight;
}
- private static int validatePort(int port, String name) {
- checkArgument(port > 0 && port <= 65535, "%s: %s (expected: 1-65535)", name, port);
- return port;
- }
-
- /**
- * Returns the IP address or host name of the ZooKeeper server.
- */
@JsonProperty
+ @Override
public String host() {
return host;
}
- /**
- * Returns the quorum port number.
- */
@JsonProperty
+ @Override
public int quorumPort() {
return quorumPort;
}
- /**
- * Returns the election port number.
- */
@JsonProperty
+ @Override
public int electionPort() {
return electionPort;
}
- /**
- * Returns the client port number.
- */
@JsonProperty
+ @Override
public int clientPort() {
return clientPort;
}
- /**
- * Returns the group ID to use hierarchical quorums.
- */
@Nullable
@JsonProperty
+ @Override
public Integer groupId() {
return groupId;
}
- /**
- * Returns the weight of the ZooKeeper server.
- */
@JsonProperty
+ @Override
public int weight() {
return weight;
}
@@ -147,11 +131,11 @@ public boolean equals(Object o) {
final ZooKeeperServerConfig that = (ZooKeeperServerConfig) o;
return host.equals(that.host) &&
- quorumPort == that.quorumPort &&
- electionPort == that.electionPort &&
- clientPort == that.clientPort &&
- Objects.equals(groupId, that.groupId) &&
- weight == that.weight;
+ quorumPort == that.quorumPort &&
+ electionPort == that.electionPort &&
+ clientPort == that.clientPort &&
+ Objects.equals(groupId, that.groupId) &&
+ weight == that.weight;
}
@Override
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperServerConfigSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperServerConfigSpec.java
new file mode 100644
index 000000000..cbde0a8b4
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/ZooKeeperServerConfigSpec.java
@@ -0,0 +1,68 @@
+/*
+ * 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.Preconditions.checkArgument;
+
+import javax.annotation.Nullable;
+
+/**
+ * Represents the address and port numbers of a ZooKeeper node.
+ */
+public interface ZooKeeperServerConfigSpec {
+ /**
+ * Validate port.
+ * @param port Port.
+ * @param name Port name to make error message.
+ * @return Validated port
+ */
+ static int validatePort(int port, String name) {
+ checkArgument(port > 0 && port <= 65535, "%s: %s (expected: 1-65535)", name, port);
+ return port;
+ }
+
+ /**
+ * Returns the IP address or host name of the ZooKeeper server.
+ */
+ String host();
+
+ /**
+ * Returns the quorum port number.
+ */
+ int quorumPort();
+
+ /**
+ * Returns the election port number.
+ */
+ int electionPort();
+
+ /**
+ * Returns the client port number.
+ */
+ int clientPort();
+
+ /**
+ * Returns the group ID to use hierarchical quorums.
+ */
+ @Nullable
+ Integer groupId();
+
+ /**
+ * Returns the weight of the ZooKeeper server.
+ */
+ int weight();
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfig.java
index 6748cad77..a01eeb559 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfig.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfig.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * 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
@@ -20,14 +20,11 @@
import static com.linecorp.centraldogma.server.internal.storage.repository.RepositoryCache.validateCacheSpec;
import static java.util.Objects.requireNonNull;
-import java.text.ParseException;
import java.util.Set;
import java.util.function.Function;
import javax.annotation.Nullable;
-import org.quartz.CronExpression;
-
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -40,23 +37,7 @@
/**
* An authentication configuration for the Central Dogma server.
*/
-public final class AuthConfig {
- /**
- * A default session timeout in milliseconds.
- */
- public static final long DEFAULT_SESSION_TIMEOUT_MILLIS = 604800000; // 7 days
-
- /**
- * A default specification for a session cache.
- */
- public static final String DEFAULT_SESSION_CACHE_SPEC =
- // Expire after the duration of session timeout.
- "maximumSize=8192,expireAfterWrite=" + (DEFAULT_SESSION_TIMEOUT_MILLIS / 1000) + 's';
-
- /**
- * A default schedule for validating sessions at 0:30, 4:30, 8:30, 12:30, 16:30 and 20:30 for every day.
- */
- public static final String DEFAULT_SESSION_VALIDATION_SCHEDULE = "0 30 */4 ? * *";
+public final class AuthConfig implements AuthConfigSpec {
private final AuthProviderFactory factory;
@@ -131,7 +112,7 @@ public AuthConfig(AuthProviderFactory factory,
checkArgument(sessionTimeoutMillis > 0,
"sessionTimeoutMillis: %s (expected: > 0)", sessionTimeoutMillis);
this.sessionTimeoutMillis = sessionTimeoutMillis;
- this.sessionValidationSchedule = validateSchedule(
+ this.sessionValidationSchedule = AuthConfigSpec.validateSchedule(
requireNonNull(sessionValidationSchedule, "sessionValidationSchedule"));
this.properties = properties;
}
@@ -139,14 +120,13 @@ public AuthConfig(AuthProviderFactory factory,
/**
* Returns the {@link AuthProviderFactory}.
*/
+ @Override
public AuthProviderFactory factory() {
return factory;
}
- /**
- * Returns the class name of the {@link AuthProviderFactory}.
- */
@JsonProperty
+ @Override
public String factoryClassName() {
return factory.getClass().getName();
}
@@ -155,63 +135,42 @@ public String factoryClassName() {
* Returns the usernames of the users with system administrator rights.
*/
@JsonProperty
+ @Override
public Set systemAdministrators() {
return systemAdministrators;
}
- /**
- * Returns whether login names are case-sensitive.
- */
@JsonProperty
+ @Override
public boolean caseSensitiveLoginNames() {
return caseSensitiveLoginNames;
}
- /**
- * Returns the spec of the session cache.
- */
@JsonProperty
+ @Override
public String sessionCacheSpec() {
return sessionCacheSpec;
}
- /**
- * Returns the timeout of an inactive session in milliseconds.
- */
@JsonProperty
+ @Override
public long sessionTimeoutMillis() {
return sessionTimeoutMillis;
}
- /**
- * Returns the cron expression that describes how often session validation task should run.
- */
@JsonProperty
+ @Override
public String sessionValidationSchedule() {
return sessionValidationSchedule;
}
- /**
- * Returns the additional properties given to the {@link AuthProviderFactory}.
- */
- @Nullable
- @JsonProperty
- public JsonNode properties() {
- return properties;
- }
-
- /**
- * Returns the additional properties, converted to {@code T}.
- */
@Nullable
+ @Override
public T properties(Class clazz) throws JsonProcessingException {
return properties != null ? Jackson.treeToValue(properties, clazz) : null;
}
- /**
- * Returns a {@link Function} which normalizes a login name based on the
- * {@link AuthConfig#caseSensitiveLoginNames()} property.
- */
+ @Override
public Function loginNameNormalizer() {
return caseSensitiveLoginNames() ? Function.identity() : Ascii::toLowerCase;
}
@@ -224,13 +183,4 @@ public String toString() {
throw new IllegalStateException(e);
}
}
-
- private static String validateSchedule(String sessionValidationSchedule) {
- try {
- CronExpression.validateExpression(sessionValidationSchedule);
- return sessionValidationSchedule;
- } catch (ParseException e) {
- throw new IllegalArgumentException("Invalid session validation schedule", e);
- }
- }
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfigSpec.java b/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfigSpec.java
new file mode 100644
index 000000000..71c794073
--- /dev/null
+++ b/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthConfigSpec.java
@@ -0,0 +1,109 @@
+/*
+ * 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.auth;
+
+import java.text.ParseException;
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.quartz.CronExpression;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+/**
+ * An authentication configuration spec for the Central Dogma server.
+ */
+public interface AuthConfigSpec {
+
+ /**
+ * A default session timeout in milliseconds.
+ */
+ long DEFAULT_SESSION_TIMEOUT_MILLIS = 604800000; // 7 days
+ /**
+ * A default specification for a session cache.
+ */
+ String DEFAULT_SESSION_CACHE_SPEC =
+ // Expire after the duration of session timeout.
+ "maximumSize=8192,expireAfterWrite=" + (DEFAULT_SESSION_TIMEOUT_MILLIS / 1000) + 's';
+ /**
+ * A default schedule for validating sessions at 0:30, 4:30, 8:30, 12:30, 16:30 and 20:30 for every day.
+ */
+ String DEFAULT_SESSION_VALIDATION_SCHEDULE = "0 30 */4 ? * *";
+
+ /**
+ * To validate session schedule.
+ * @param sessionValidationSchedule Target session schedule.
+ * @return Validated session schedule.
+ */
+ static String validateSchedule(String sessionValidationSchedule) {
+ try {
+ CronExpression.validateExpression(sessionValidationSchedule);
+ return sessionValidationSchedule;
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Invalid session validation schedule", e);
+ }
+ }
+
+ /**
+ * Returns the {@link AuthProviderFactory}.
+ */
+ AuthProviderFactory factory();
+
+ /**
+ * Returns the class name of the {@link AuthProviderFactory}.
+ */
+ String factoryClassName();
+
+ /**
+ * Returns the usernames of the users with administrator rights.
+ */
+ Set systemAdministrators();
+
+ /**
+ * Returns whether login names are case-sensitive.
+ */
+ boolean caseSensitiveLoginNames();
+
+ /**
+ * Returns the spec of the session cache.
+ */
+ String sessionCacheSpec();
+
+ /**
+ * Returns the timeout of an inactive session in milliseconds.
+ */
+ long sessionTimeoutMillis();
+
+ /**
+ * Returns the cron expression that describes how often session validation task should run.
+ */
+ String sessionValidationSchedule();
+
+ /**
+ * Returns the additional properties, converted to {@code T}.
+ */
+ @Nullable
+ T properties(Class clazz) throws JsonProcessingException;
+
+ /**
+ * Returns a {@link Function} which normalizes a login name based on the
+ * {@link AuthConfigSpec#caseSensitiveLoginNames()} property.
+ */
+ Function loginNameNormalizer();
+}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthProviderParameters.java b/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthProviderParameters.java
index b606e0ba6..665bda1ad 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthProviderParameters.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/auth/AuthProviderParameters.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * 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
@@ -23,7 +23,7 @@
import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.server.auth.Authorizer;
-import com.linecorp.centraldogma.server.CentralDogmaConfig;
+import com.linecorp.centraldogma.server.CentralDogmaConfigSpec;
/**
* Parameters which are used to create a new {@link AuthProvider} instance.
@@ -31,8 +31,8 @@
public final class AuthProviderParameters {
private final Authorizer authorizer;
- private final CentralDogmaConfig config;
- private final AuthConfig authConfig;
+ private final CentralDogmaConfigSpec config;
+ private final AuthConfigSpec authConfig;
private final Supplier sessionIdGenerator;
private final Function> loginSessionPropagator;
private final Function> logoutSessionPropagator;
@@ -50,7 +50,7 @@ public final class AuthProviderParameters {
*/
public AuthProviderParameters(
Authorizer authorizer,
- CentralDogmaConfig config,
+ CentralDogmaConfigSpec config,
Supplier sessionIdGenerator,
Function> loginSessionPropagator,
Function> logoutSessionPropagator) {
@@ -72,14 +72,14 @@ public Authorizer authorizer() {
/**
* Returns the configuration for the Central Dogma server.
*/
- public CentralDogmaConfig config() {
+ public CentralDogmaConfigSpec config() {
return config;
}
/**
* Returns the authentication configuration.
*/
- public AuthConfig authConfig() {
+ public AuthConfigSpec authConfig() {
return authConfig;
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/auth/Session.java b/server/src/main/java/com/linecorp/centraldogma/server/auth/Session.java
index ecc784e0c..c39ef0570 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/auth/Session.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/auth/Session.java
@@ -53,11 +53,23 @@ public final class Session {
* @param sessionValidDuration the {@link Duration} that this session is valid
*/
public Session(String id, String username, Duration sessionValidDuration) {
+ this(id, username, sessionValidDuration, null);
+ }
+
+ /**
+ * Creates a new {@link Session} instance.
+ *
+ * @param id the session ID
+ * @param username the name of the user which belongs to this session
+ * @param sessionValidDuration the {@link Duration} that this session is valid
+ * @param rawSession the serializable session object which is specific to authentication provider
+ */
+ public Session(String id, String username, Duration sessionValidDuration, Serializable rawSession) {
this.id = requireNonNull(id, "id");
this.username = requireNonNull(username, "username");
creationTime = Instant.now();
expirationTime = creationTime.plus(requireNonNull(sessionValidDuration, "sessionValidDuration"));
- rawSession = null;
+ this.rawSession = rawSession;
}
/**
@@ -132,7 +144,7 @@ public Serializable rawSession() {
* @throws NullPointerException if the {@code rawSession} is {@code null}
* @throws ClassCastException if the {@code rawSession} cannot be casted to {@code T}
*/
- T castRawSession() {
+ public T castRawSession() {
return Util.unsafeCast(requireNonNull(rawSession, "rawSession"));
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/command/CommandExecutor.java b/server/src/main/java/com/linecorp/centraldogma/server/command/CommandExecutor.java
index 8c9307c03..39f9724ae 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/command/CommandExecutor.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/command/CommandExecutor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * 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
@@ -20,7 +20,7 @@
import javax.annotation.Nullable;
-import com.linecorp.centraldogma.server.QuotaConfig;
+import com.linecorp.centraldogma.server.QuotaConfigSpec;
/**
* An executor interface which executes {@link Command}s.
@@ -61,10 +61,10 @@ public interface CommandExecutor {
void setWritable(boolean writable);
/**
- * Sets the specified {@linkplain QuotaConfig write quota} to the specified {@code repoName} in the
+ * Sets the specified {@linkplain QuotaConfigSpec write quota} to the specified {@code repoName} in the
* specified {@code projectName}.
*/
- void setWriteQuota(String projectName, String repoName, @Nullable QuotaConfig writeQuota);
+ void setWriteQuota(String projectName, String repoName, @Nullable QuotaConfigSpec writeQuota);
/**
* Executes the specified {@link Command}.
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/command/ForwardingCommandExecutor.java b/server/src/main/java/com/linecorp/centraldogma/server/command/ForwardingCommandExecutor.java
index 7330bb614..5cca94267 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/command/ForwardingCommandExecutor.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/command/ForwardingCommandExecutor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * 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
@@ -21,7 +21,7 @@
import java.util.concurrent.CompletableFuture;
import com.linecorp.centraldogma.internal.Util;
-import com.linecorp.centraldogma.server.QuotaConfig;
+import com.linecorp.centraldogma.server.QuotaConfigSpec;
/**
* A {@link CommandExecutor} which forwards all its method calls to another {@link CommandExecutor}.
@@ -69,7 +69,7 @@ public void setWritable(boolean writable) {
}
@Override
- public void setWriteQuota(String projectName, String repoName, QuotaConfig writeQuota) {
+ public void setWriteQuota(String projectName, String repoName, QuotaConfigSpec writeQuota) {
delegate().setWriteQuota(projectName, repoName, writeQuota);
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/command/StandaloneCommandExecutor.java b/server/src/main/java/com/linecorp/centraldogma/server/command/StandaloneCommandExecutor.java
index b1cd02f3e..943788288 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/command/StandaloneCommandExecutor.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/command/StandaloneCommandExecutor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * 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
@@ -34,7 +34,7 @@
import com.spotify.futures.CompletableFutures;
import com.linecorp.centraldogma.common.TooManyRequestsException;
-import com.linecorp.centraldogma.server.QuotaConfig;
+import com.linecorp.centraldogma.server.QuotaConfigSpec;
import com.linecorp.centraldogma.server.auth.Session;
import com.linecorp.centraldogma.server.auth.SessionManager;
import com.linecorp.centraldogma.server.management.ServerStatusManager;
@@ -81,7 +81,7 @@ public StandaloneCommandExecutor(ProjectManager projectManager,
Executor repositoryWorker,
ServerStatusManager serverStatusManager,
@Nullable SessionManager sessionManager,
- @Nullable QuotaConfig writeQuota,
+ @Nullable QuotaConfigSpec writeQuota,
@Nullable Consumer onTakeLeadership,
@Nullable Consumer onReleaseLeadership,
@Nullable Consumer onTakeZoneLeadership,
@@ -341,7 +341,7 @@ private CompletableFuture getRateLimiter(String projectName, String
}
@Override
- public final void setWriteQuota(String projectName, String repoName, @Nullable QuotaConfig writeQuota) {
+ public final void setWriteQuota(String projectName, String repoName, @Nullable QuotaConfigSpec writeQuota) {
if (!writeQuotaEnabled()) {
// This method should be called only when a write quota is enabled.
return;
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/admin/auth/FileBasedSessionManager.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/admin/auth/FileBasedSessionManager.java
index aefaefffa..23844379c 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/admin/auth/FileBasedSessionManager.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/admin/auth/FileBasedSessionManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * 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
@@ -53,7 +53,7 @@
import com.google.common.collect.ImmutableMap;
import com.linecorp.centraldogma.internal.Jackson;
-import com.linecorp.centraldogma.server.auth.AuthConfig;
+import com.linecorp.centraldogma.server.auth.AuthConfigSpec;
import com.linecorp.centraldogma.server.auth.AuthException;
import com.linecorp.centraldogma.server.auth.Session;
import com.linecorp.centraldogma.server.auth.SessionManager;
@@ -61,7 +61,7 @@
/**
* A {@link SessionManager} based on the file system. The sessions stored in the file system would be
* deleted when the {@link #delete(String)} method is called or when the {@link ExpiredSessionDeletingJob}
- * finds the expired session. The {@link AuthConfig#sessionValidationSchedule()} can configure
+ * finds the expired session. The {@link AuthConfigSpec#sessionValidationSchedule()} can configure
* the schedule for deleting expired sessions.
*/
public final class FileBasedSessionManager implements SessionManager {
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java
index 74003c8d6..0771858e1 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * 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
@@ -45,6 +45,7 @@
import com.linecorp.centraldogma.internal.jsonpatch.JsonPatchOperation;
import com.linecorp.centraldogma.internal.jsonpatch.ReplaceOperation;
import com.linecorp.centraldogma.server.QuotaConfig;
+import com.linecorp.centraldogma.server.QuotaConfigSpec;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresProjectRole;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresSystemAdministrator;
@@ -250,7 +251,7 @@ public CompletableFuture removeTokenRepositoryRole(@Param String proje
/**
* PATCH /metadata/{projectName}/repos/{repoName}/quota/write
*
- *
Updates the {@linkplain QuotaConfig write quota} for the specified {@code repoName}
+ *
Updates the {@linkplain QuotaConfigSpec write quota} for the specified {@code repoName}
* in the specified {@code projectName}.
*/
@Patch("/metadata/{projectName}/repos/{repoName}/quota/write")
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java
index e8029de3e..e92695433 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java
@@ -48,8 +48,8 @@
import com.linecorp.centraldogma.common.Revision;
import com.linecorp.centraldogma.internal.api.v1.MirrorDto;
import com.linecorp.centraldogma.internal.api.v1.PushResultDto;
-import com.linecorp.centraldogma.server.CentralDogmaConfig;
-import com.linecorp.centraldogma.server.ZoneConfig;
+import com.linecorp.centraldogma.server.CentralDogmaConfigSpec;
+import com.linecorp.centraldogma.server.ZoneConfigSpec;
import com.linecorp.centraldogma.server.command.Command;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.command.CommitResult;
@@ -77,10 +77,10 @@ public class MirroringServiceV1 extends AbstractService {
private final MirrorRunner mirrorRunner;
private final Map mirrorZoneConfig;
@Nullable
- private final ZoneConfig zoneConfig;
+ private final ZoneConfigSpec zoneConfig;
public MirroringServiceV1(ProjectApiManager projectApiManager, CommandExecutor executor,
- MirrorRunner mirrorRunner, CentralDogmaConfig config) {
+ MirrorRunner mirrorRunner, CentralDogmaConfigSpec config) {
super(executor);
this.projectApiManager = projectApiManager;
this.mirrorRunner = mirrorRunner;
@@ -88,12 +88,12 @@ public MirroringServiceV1(ProjectApiManager projectApiManager, CommandExecutor e
mirrorZoneConfig = mirrorZoneConfig(config);
}
- private static Map mirrorZoneConfig(CentralDogmaConfig config) {
+ private static Map mirrorZoneConfig(CentralDogmaConfigSpec config) {
final MirroringServicePluginConfig mirrorConfig = mirrorConfig(config);
final ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(2);
final boolean zonePinned = mirrorConfig != null && mirrorConfig.zonePinned();
builder.put("zonePinned", zonePinned);
- final ZoneConfig zone = config.zone();
+ final ZoneConfigSpec zone = config.zone();
if (zone != null) {
builder.put("zone", zone);
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServicePlugin.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServicePlugin.java
index b3fbbe7b6..738e1e3c9 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServicePlugin.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMirroringServicePlugin.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2019 LINE Corporation
+ * 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
@@ -25,17 +25,19 @@
import com.google.common.base.MoreObjects;
-import com.linecorp.centraldogma.server.CentralDogmaConfig;
-import com.linecorp.centraldogma.server.ZoneConfig;
+import com.linecorp.centraldogma.server.CentralDogmaConfigSpec;
+import com.linecorp.centraldogma.server.ZoneConfigSpec;
import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfig;
+import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfigSpec;
import com.linecorp.centraldogma.server.plugin.Plugin;
+import com.linecorp.centraldogma.server.plugin.PluginConfig;
import com.linecorp.centraldogma.server.plugin.PluginContext;
import com.linecorp.centraldogma.server.plugin.PluginTarget;
public final class DefaultMirroringServicePlugin implements Plugin {
@Nullable
- public static MirroringServicePluginConfig mirrorConfig(CentralDogmaConfig config) {
+ public static MirroringServicePluginConfig mirrorConfig(CentralDogmaConfigSpec config) {
return (MirroringServicePluginConfig) config.pluginConfigMap().get(MirroringServicePluginConfig.class);
}
@@ -46,7 +48,7 @@ public static MirroringServicePluginConfig mirrorConfig(CentralDogmaConfig confi
private PluginTarget pluginTarget;
@Override
- public PluginTarget target(CentralDogmaConfig config) {
+ public PluginTarget target(CentralDogmaConfigSpec config) {
requireNonNull(config, "config");
if (pluginTarget != null) {
return pluginTarget;
@@ -67,12 +69,12 @@ public synchronized CompletionStage start(PluginContext context) {
MirrorSchedulingService mirroringService = this.mirroringService;
if (mirroringService == null) {
- final CentralDogmaConfig cfg = context.config();
+ final CentralDogmaConfigSpec cfg = context.config();
final MirroringServicePluginConfig mirroringServicePluginConfig = mirrorConfig(cfg);
final int numThreads;
final int maxNumFilesPerMirror;
final long maxNumBytesPerMirror;
- final ZoneConfig zoneConfig;
+ final ZoneConfigSpec zoneConfig;
if (mirroringServicePluginConfig != null) {
numThreads = mirroringServicePluginConfig.numMirroringThreads();
@@ -112,8 +114,8 @@ public synchronized CompletionStage stop(PluginContext context) {
}
@Override
- public Class> configType() {
- return MirroringServicePluginConfig.class;
+ public Class extends PluginConfig> configType() {
+ return MirroringServicePluginConfigSpec.class;
}
@Nullable
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorRunner.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorRunner.java
index 8e0079cee..42b8188ca 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorRunner.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorRunner.java
@@ -35,7 +35,7 @@
import com.linecorp.armeria.common.util.SafeCloseable;
import com.linecorp.centraldogma.common.MirrorException;
-import com.linecorp.centraldogma.server.CentralDogmaConfig;
+import com.linecorp.centraldogma.server.CentralDogmaConfigSpec;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager;
import com.linecorp.centraldogma.server.metadata.User;
@@ -62,7 +62,7 @@ public final class MirrorRunner implements SafeCloseable {
private final String currentZone;
public MirrorRunner(ProjectApiManager projectApiManager, CommandExecutor commandExecutor,
- CentralDogmaConfig cfg, MeterRegistry meterRegistry) {
+ CentralDogmaConfigSpec cfg, MeterRegistry meterRegistry) {
this.projectApiManager = projectApiManager;
this.commandExecutor = commandExecutor;
// TODO(ikhoon): Periodically clean up stale repositories.
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java
index 4dc69f743..72628e230 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingService.java
@@ -52,7 +52,7 @@
import com.linecorp.centraldogma.common.MirrorException;
import com.linecorp.centraldogma.server.MirroringService;
-import com.linecorp.centraldogma.server.ZoneConfig;
+import com.linecorp.centraldogma.server.ZoneConfigSpec;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.metadata.User;
import com.linecorp.centraldogma.server.mirror.Mirror;
@@ -93,7 +93,7 @@ public final class MirrorSchedulingService implements MirroringService {
private final int maxNumFilesPerMirror;
private final long maxNumBytesPerMirror;
@Nullable
- private final ZoneConfig zoneConfig;
+ private final ZoneConfigSpec zoneConfig;
@Nullable
private final String currentZone;
@@ -107,7 +107,7 @@ public final class MirrorSchedulingService implements MirroringService {
@VisibleForTesting
public MirrorSchedulingService(File workDir, ProjectManager projectManager, MeterRegistry meterRegistry,
int numThreads, int maxNumFilesPerMirror, long maxNumBytesPerMirror,
- @Nullable ZoneConfig zoneConfig) {
+ @Nullable ZoneConfigSpec zoneConfig) {
this.workDir = requireNonNull(workDir, "workDir");
this.projectManager = requireNonNull(projectManager, "projectManager");
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutor.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutor.java
index b3e674221..c65eafd33 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutor.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/replication/ZooKeeperCommandExecutor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * 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
@@ -97,8 +97,9 @@
import com.linecorp.centraldogma.common.TooManyRequestsException;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.server.QuotaConfig;
-import com.linecorp.centraldogma.server.ZooKeeperReplicationConfig;
-import com.linecorp.centraldogma.server.ZooKeeperServerConfig;
+import com.linecorp.centraldogma.server.QuotaConfigSpec;
+import com.linecorp.centraldogma.server.ZooKeeperReplicationConfigSpec;
+import com.linecorp.centraldogma.server.ZooKeeperServerConfigSpec;
import com.linecorp.centraldogma.server.command.AbstractCommandExecutor;
import com.linecorp.centraldogma.server.command.Command;
import com.linecorp.centraldogma.server.command.CommandExecutor;
@@ -159,9 +160,9 @@ public final class ZooKeeperCommandExecutor
new ConcurrentHashMap<>();
@VisibleForTesting
- final Cache writeQuotaCache = Caffeine.newBuilder().maximumSize(2000).build();
+ final Cache writeQuotaCache = Caffeine.newBuilder().maximumSize(2000).build();
- private final ZooKeeperReplicationConfig cfg;
+ private final ZooKeeperReplicationConfigSpec cfg;
private final File revisionFile;
private final File zkConfFile;
private final File zkDataDir;
@@ -170,7 +171,7 @@ public final class ZooKeeperCommandExecutor
private final MeterRegistry meterRegistry;
@Nullable
- private final QuotaConfig writeQuota;
+ private final QuotaConfigSpec writeQuota;
@Nullable
private final String zone;
@@ -405,11 +406,11 @@ private static final class ListenerInfo {
private volatile ListenerInfo listenerInfo;
- public ZooKeeperCommandExecutor(ZooKeeperReplicationConfig cfg,
+ public ZooKeeperCommandExecutor(ZooKeeperReplicationConfigSpec cfg,
File dataDir, CommandExecutor delegate,
MeterRegistry meterRegistry,
ProjectManager projectManager,
- @Nullable QuotaConfig writeQuota,
+ @Nullable QuotaConfigSpec writeQuota,
@Nullable String zone,
@Nullable Consumer onTakeLeadership,
@Nullable Consumer onReleaseLeadership,
@@ -626,11 +627,11 @@ private EmbeddedZooKeeper startZooKeeper() throws Exception {
// Set the client port, which is unused.
zkProps.setProperty("clientPort", String.valueOf(cfg.serverConfig().clientPort()));
- final Map servers = cfg.servers();
+ final Map servers = cfg.servers();
// Add replicas.
boolean hasGroupId = false;
- for (Entry entry : servers.entrySet()) {
- final ZooKeeperServerConfig serverConfig = entry.getValue();
+ for (Entry entry : servers.entrySet()) {
+ final ZooKeeperServerConfigSpec serverConfig = entry.getValue();
zkProps.setProperty(
"server." + entry.getKey(),
serverConfig.host() + ':' + serverConfig.quorumPort() + ':' +
@@ -645,11 +646,11 @@ private EmbeddedZooKeeper startZooKeeper() throws Exception {
if (hasGroupId) {
final ImmutableMultimap.Builder groupBuilder = ImmutableMultimap.builder();
boolean isHierarchical = true;
- for (Entry entry : servers.entrySet()) {
+ for (Entry entry : servers.entrySet()) {
final Integer groupId = entry.getValue().groupId();
if (groupId == null) {
isHierarchical = false;
- final List noGroupIds =
+ final List