Create spring-boot-couchbase module

This commit is contained in:
Stéphane Nicoll
2025-03-24 11:30:53 +01:00
committed by Phillip Webb
parent abfd521363
commit 42f12380da
40 changed files with 225 additions and 183 deletions

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed 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 org.springframework.boot.couchbase.autoconfigure;
import java.time.Duration;
import com.couchbase.client.core.diagnostics.ClusterState;
import com.couchbase.client.core.diagnostics.DiagnosticsResult;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.Collection;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.json.JsonObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.testcontainers.couchbase.BucketDefinition;
import org.testcontainers.couchbase.CouchbaseContainer;
import org.testcontainers.couchbase.CouchbaseService;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.container.TestImage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link CouchbaseAutoConfiguration}.
*
* @author Stephane Nicoll
* @author Brian Clozel
*/
@Testcontainers(disabledWithoutDocker = true)
class CouchbaseAutoConfigurationIntegrationTests {
private static final String BUCKET_NAME = "cbbucket";
@Container
static final CouchbaseContainer couchbase = TestImage.container(CouchbaseContainer.class)
.withEnabledServices(CouchbaseService.KV)
.withCredentials("spring", "password")
.withBucket(new BucketDefinition(BUCKET_NAME).withPrimaryIndex(false));
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CouchbaseAutoConfiguration.class))
.withPropertyValues("spring.couchbase.connection-string: " + couchbase.getConnectionString(),
"spring.couchbase.username:spring", "spring.couchbase.password:password",
"spring.couchbase.bucket.name:" + BUCKET_NAME, "spring.couchbase.env.timeouts.connect=2m",
"spring.couchbase.env.timeouts.key-value=1m");
@Test
void defaultConfiguration() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(Cluster.class).hasSingleBean(ClusterEnvironment.class);
Cluster cluster = context.getBean(Cluster.class);
Bucket bucket = cluster.bucket(BUCKET_NAME);
bucket.waitUntilReady(Duration.ofMinutes(5));
DiagnosticsResult diagnostics = cluster.diagnostics();
assertThat(diagnostics.state()).isEqualTo(ClusterState.ONLINE);
});
}
@Test
void whenCouchbaseIsUsingCustomObjectMapperThenJsonCanBeRoundTripped() {
this.contextRunner.withBean(ObjectMapper.class, ObjectMapper::new).run((context) -> {
Cluster cluster = context.getBean(Cluster.class);
Bucket bucket = cluster.bucket(BUCKET_NAME);
bucket.waitUntilReady(Duration.ofMinutes(5));
Collection collection = bucket.defaultCollection();
collection.insert("test-document", JsonObject.create().put("a", "alpha"));
assertThat(collection.get("test-document").contentAsObject().get("a")).isEqualTo("alpha");
});
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed 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 org.springframework.boot.couchbase.autoconfigure;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.env.ClusterEnvironment.Builder;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link ClusterEnvironment} through a {@link Builder ClusterEnvironment.Builder} whilst
* retaining default auto-configuration.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
@FunctionalInterface
public interface ClusterEnvironmentBuilderCustomizer {
/**
* Customize the {@link Builder ClusterEnvironment.Builder}.
* @param builder the builder to customize
*/
void customize(ClusterEnvironment.Builder builder);
}

View File

@@ -0,0 +1,280 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed 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 org.springframework.boot.couchbase.autoconfigure;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import javax.net.ssl.TrustManagerFactory;
import com.couchbase.client.core.env.Authenticator;
import com.couchbase.client.core.env.CertificateAuthenticator;
import com.couchbase.client.core.env.PasswordAuthenticator;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.ClusterOptions;
import com.couchbase.client.java.codec.JacksonJsonSerializer;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.env.ClusterEnvironment.Builder;
import com.couchbase.client.java.json.JsonValueModule;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.couchbase.autoconfigure.CouchbaseAutoConfiguration.CouchbaseCondition;
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Authentication.Jks;
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Authentication.Pem;
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Ssl;
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Timeouts;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.pem.PemSslStore;
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Couchbase.
*
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Yulin Qin
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
* @since 4.0.0
*/
@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration")
@ConditionalOnClass(Cluster.class)
@Conditional(CouchbaseCondition.class)
@EnableConfigurationProperties(CouchbaseProperties.class)
public class CouchbaseAutoConfiguration {
private final ResourceLoader resourceLoader;
private final CouchbaseProperties properties;
CouchbaseAutoConfiguration(ResourceLoader resourceLoader, CouchbaseProperties properties) {
this.resourceLoader = ApplicationResourceLoader.get(resourceLoader);
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean(CouchbaseConnectionDetails.class)
PropertiesCouchbaseConnectionDetails couchbaseConnectionDetails(ObjectProvider<SslBundles> sslBundles) {
return new PropertiesCouchbaseConnectionDetails(this.properties, sslBundles.getIfAvailable());
}
@Bean
@ConditionalOnMissingBean
public ClusterEnvironment couchbaseClusterEnvironment(
ObjectProvider<ClusterEnvironmentBuilderCustomizer> customizers,
CouchbaseConnectionDetails connectionDetails) {
Builder builder = initializeEnvironmentBuilder(connectionDetails);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
@Bean
@ConditionalOnMissingBean
public Authenticator couchbaseAuthenticator(CouchbaseConnectionDetails connectionDetails) throws IOException {
if (connectionDetails.getUsername() != null && connectionDetails.getPassword() != null) {
return PasswordAuthenticator.create(connectionDetails.getUsername(), connectionDetails.getPassword());
}
Pem pem = this.properties.getAuthentication().getPem();
if (pem.getCertificates() != null) {
PemSslStoreDetails details = new PemSslStoreDetails(null, pem.getCertificates(), pem.getPrivateKey());
PemSslStore store = PemSslStore.load(details);
return CertificateAuthenticator.fromKey(store.privateKey(), pem.getPrivateKeyPassword(),
store.certificates());
}
Jks jks = this.properties.getAuthentication().getJks();
if (jks.getLocation() != null) {
Resource resource = this.resourceLoader.getResource(jks.getLocation());
String keystorePassword = jks.getPassword();
try (InputStream inputStream = resource.getInputStream()) {
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
store.load(inputStream, (keystorePassword != null) ? keystorePassword.toCharArray() : null);
return CertificateAuthenticator.fromKeyStore(store, keystorePassword);
}
catch (GeneralSecurityException ex) {
throw new IllegalStateException("Error reading Couchbase certificate store", ex);
}
}
throw new IllegalStateException("Couchbase authentication requires username and password, or certificates");
}
@Bean(destroyMethod = "disconnect")
@ConditionalOnMissingBean
public Cluster couchbaseCluster(ClusterEnvironment couchbaseClusterEnvironment, Authenticator authenticator,
CouchbaseConnectionDetails connectionDetails) {
ClusterOptions options = ClusterOptions.clusterOptions(authenticator).environment(couchbaseClusterEnvironment);
return Cluster.connect(connectionDetails.getConnectionString(), options);
}
private ClusterEnvironment.Builder initializeEnvironmentBuilder(CouchbaseConnectionDetails connectionDetails) {
ClusterEnvironment.Builder builder = ClusterEnvironment.builder();
Timeouts timeouts = this.properties.getEnv().getTimeouts();
builder.timeoutConfig((config) -> config.kvTimeout(timeouts.getKeyValue())
.analyticsTimeout(timeouts.getAnalytics())
.kvDurableTimeout(timeouts.getKeyValueDurable())
.queryTimeout(timeouts.getQuery())
.viewTimeout(timeouts.getView())
.searchTimeout(timeouts.getSearch())
.managementTimeout(timeouts.getManagement())
.connectTimeout(timeouts.getConnect())
.disconnectTimeout(timeouts.getDisconnect()));
CouchbaseProperties.Io io = this.properties.getEnv().getIo();
builder.ioConfig((config) -> config.maxHttpConnections(io.getMaxEndpoints())
.numKvConnections(io.getMinEndpoints())
.idleHttpConnectionTimeout(io.getIdleHttpConnectionTimeout()));
SslBundle sslBundle = connectionDetails.getSslBundle();
if (sslBundle != null) {
configureSsl(builder, sslBundle);
}
return builder;
}
private void configureSsl(Builder builder, SslBundle sslBundle) {
Assert.state(!sslBundle.getOptions().isSpecified(), "SSL Options cannot be specified with Couchbase");
builder.securityConfig((config) -> {
config.enableTls(true);
TrustManagerFactory trustManagerFactory = sslBundle.getManagers().getTrustManagerFactory();
if (trustManagerFactory != null) {
config.trustManagerFactory(trustManagerFactory);
}
});
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
static class JacksonConfiguration {
@Bean
@ConditionalOnSingleCandidate(ObjectMapper.class)
ClusterEnvironmentBuilderCustomizer jacksonClusterEnvironmentBuilderCustomizer(ObjectMapper objectMapper) {
return new JacksonClusterEnvironmentBuilderCustomizer(
objectMapper.copy().registerModule(new JsonValueModule()));
}
}
private static final class JacksonClusterEnvironmentBuilderCustomizer
implements ClusterEnvironmentBuilderCustomizer, Ordered {
private final ObjectMapper objectMapper;
private JacksonClusterEnvironmentBuilderCustomizer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void customize(Builder builder) {
builder.jsonSerializer(JacksonJsonSerializer.create(this.objectMapper));
}
@Override
public int getOrder() {
return 0;
}
}
/**
* Condition that matches when {@code spring.couchbase.connection-string} has been
* configured or there is a {@link CouchbaseConnectionDetails} bean.
*/
static final class CouchbaseCondition extends AnyNestedCondition {
CouchbaseCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty("spring.couchbase.connection-string")
private static final class CouchbaseUrlCondition {
}
@ConditionalOnBean(CouchbaseConnectionDetails.class)
private static final class CouchbaseConnectionDetailsCondition {
}
}
/**
* Adapts {@link CouchbaseProperties} to {@link CouchbaseConnectionDetails}.
*/
static final class PropertiesCouchbaseConnectionDetails implements CouchbaseConnectionDetails {
private final CouchbaseProperties properties;
private final SslBundles sslBundles;
PropertiesCouchbaseConnectionDetails(CouchbaseProperties properties, SslBundles sslBundles) {
this.properties = properties;
this.sslBundles = sslBundles;
}
@Override
public String getConnectionString() {
return this.properties.getConnectionString();
}
@Override
public String getUsername() {
return this.properties.getUsername();
}
@Override
public String getPassword() {
return this.properties.getPassword();
}
@Override
public SslBundle getSslBundle() {
Ssl ssl = this.properties.getEnv().getSsl();
if (!ssl.getEnabled()) {
return null;
}
if (StringUtils.hasLength(ssl.getBundle())) {
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
return this.sslBundles.getBundle(ssl.getBundle());
}
return SslBundle.systemDefault();
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed 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 org.springframework.boot.couchbase.autoconfigure;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
/**
* Details required to establish a connection to a Couchbase service.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 4.0.0
*/
public interface CouchbaseConnectionDetails extends ConnectionDetails {
/**
* Connection string used to locate the Couchbase cluster.
* @return the connection string used to locate the Couchbase cluster
*/
String getConnectionString();
/**
* Cluster username.
* @return the cluster username
*/
String getUsername();
/**
* Cluster password.
* @return the cluster password
*/
String getPassword();
/**
* SSL bundle to use.
* @return the SSL bundle to use
* @since 3.5.0
*/
default SslBundle getSslBundle() {
return null;
}
}

View File

@@ -0,0 +1,409 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed 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 org.springframework.boot.couchbase.autoconfigure;
import java.time.Duration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
/**
* Configuration properties for Couchbase.
*
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Yulin Qin
* @author Brian Clozel
* @author Michael Nitschinger
* @author Scott Frederick
* @since 4.0.0
*/
@ConfigurationProperties("spring.couchbase")
public class CouchbaseProperties {
/**
* Connection string used to locate the Couchbase cluster.
*/
private String connectionString;
/**
* Cluster username.
*/
private String username;
/**
* Cluster password.
*/
private String password;
private final Authentication authentication = new Authentication();
private final Env env = new Env();
public String getConnectionString() {
return this.connectionString;
}
public void setConnectionString(String connectionString) {
this.connectionString = connectionString;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public Authentication getAuthentication() {
return this.authentication;
}
public Env getEnv() {
return this.env;
}
public static class Authentication {
private final Pem pem = new Pem();
private final Jks jks = new Jks();
public Pem getPem() {
return this.pem;
}
public Jks getJks() {
return this.jks;
}
public static class Pem {
/**
* PEM-formatted certificates for certificate-based cluster authentication.
*/
private String certificates;
/**
* PEM-formatted private key for certificate-based cluster authentication.
*/
private String privateKey;
/**
* Private key password for certificate-based cluster authentication.
*/
private String privateKeyPassword;
public String getCertificates() {
return this.certificates;
}
public void setCertificates(String certificates) {
this.certificates = certificates;
}
public String getPrivateKey() {
return this.privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public String getPrivateKeyPassword() {
return this.privateKeyPassword;
}
public void setPrivateKeyPassword(String privateKeyPassword) {
this.privateKeyPassword = privateKeyPassword;
}
}
public static class Jks {
/**
* Java KeyStore location for certificate-based cluster authentication.
*/
private String location;
/**
* Java KeyStore password for certificate-based cluster authentication.
*/
private String password;
/**
* Private key password for certificate-based cluster authentication.
*/
private String privateKeyPassword;
public String getLocation() {
return this.location;
}
public void setLocation(String location) {
this.location = location;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPrivateKeyPassword() {
return this.privateKeyPassword;
}
public void setPrivateKeyPassword(String privateKeyPassword) {
this.privateKeyPassword = privateKeyPassword;
}
}
}
public static class Env {
private final Io io = new Io();
private final Ssl ssl = new Ssl();
private final Timeouts timeouts = new Timeouts();
public Io getIo() {
return this.io;
}
public Ssl getSsl() {
return this.ssl;
}
public Timeouts getTimeouts() {
return this.timeouts;
}
}
public static class Io {
/**
* Minimum number of sockets per node.
*/
private int minEndpoints = 1;
/**
* Maximum number of sockets per node.
*/
private int maxEndpoints = 12;
/**
* Length of time an HTTP connection may remain idle before it is closed and
* removed from the pool.
*/
private Duration idleHttpConnectionTimeout = Duration.ofSeconds(1);
public int getMinEndpoints() {
return this.minEndpoints;
}
public void setMinEndpoints(int minEndpoints) {
this.minEndpoints = minEndpoints;
}
public int getMaxEndpoints() {
return this.maxEndpoints;
}
public void setMaxEndpoints(int maxEndpoints) {
this.maxEndpoints = maxEndpoints;
}
public Duration getIdleHttpConnectionTimeout() {
return this.idleHttpConnectionTimeout;
}
public void setIdleHttpConnectionTimeout(Duration idleHttpConnectionTimeout) {
this.idleHttpConnectionTimeout = idleHttpConnectionTimeout;
}
}
public static class Ssl {
/**
* Whether to enable SSL support. Enabled automatically if a "bundle" is provided
* unless specified otherwise.
*/
private Boolean enabled;
/**
* SSL bundle name.
*/
private String bundle;
public Boolean getEnabled() {
return (this.enabled != null) ? this.enabled : StringUtils.hasText(this.bundle);
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getBundle() {
return this.bundle;
}
public void setBundle(String bundle) {
this.bundle = bundle;
}
}
public static class Timeouts {
/**
* Bucket connect timeout.
*/
private Duration connect = Duration.ofSeconds(10);
/**
* Bucket disconnect timeout.
*/
private Duration disconnect = Duration.ofSeconds(10);
/**
* Timeout for operations on a specific key-value.
*/
private Duration keyValue = Duration.ofMillis(2500);
/**
* Timeout for operations on a specific key-value with a durability level.
*/
private Duration keyValueDurable = Duration.ofSeconds(10);
/**
* N1QL query operations timeout.
*/
private Duration query = Duration.ofSeconds(75);
/**
* Regular and geospatial view operations timeout.
*/
private Duration view = Duration.ofSeconds(75);
/**
* Timeout for the search service.
*/
private Duration search = Duration.ofSeconds(75);
/**
* Timeout for the analytics service.
*/
private Duration analytics = Duration.ofSeconds(75);
/**
* Timeout for the management operations.
*/
private Duration management = Duration.ofSeconds(75);
public Duration getConnect() {
return this.connect;
}
public void setConnect(Duration connect) {
this.connect = connect;
}
public Duration getDisconnect() {
return this.disconnect;
}
public void setDisconnect(Duration disconnect) {
this.disconnect = disconnect;
}
public Duration getKeyValue() {
return this.keyValue;
}
public void setKeyValue(Duration keyValue) {
this.keyValue = keyValue;
}
public Duration getKeyValueDurable() {
return this.keyValueDurable;
}
public void setKeyValueDurable(Duration keyValueDurable) {
this.keyValueDurable = keyValueDurable;
}
public Duration getQuery() {
return this.query;
}
public void setQuery(Duration query) {
this.query = query;
}
public Duration getView() {
return this.view;
}
public void setView(Duration view) {
this.view = view;
}
public Duration getSearch() {
return this.search;
}
public void setSearch(Duration search) {
this.search = search;
}
public Duration getAnalytics() {
return this.analytics;
}
public void setAnalytics(Duration analytics) {
this.analytics = analytics;
}
public Duration getManagement() {
return this.management;
}
public void setManagement(Duration management) {
this.management = management;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed 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.
*/
/**
* Auto-configuration for Couchbase.
*/
package org.springframework.boot.couchbase.autoconfigure;

View File

@@ -0,0 +1,136 @@
{
"groups": [],
"properties": [
{
"name": "spring.couchbase.bootstrap-hosts",
"type": "java.util.List<java.lang.String>",
"description": "Couchbase nodes (host or IP address) to bootstrap from.",
"deprecation": {
"replacement": "spring.couchbase.connection-string",
"level": "error"
}
},
{
"name": "spring.couchbase.bucket.name",
"type": "java.lang.String",
"description": "Name of the bucket to connect to.",
"deprecation": {
"reason": "A bucket is no longer auto-configured.",
"level": "error"
}
},
{
"name": "spring.couchbase.bucket.password",
"type": "java.lang.String",
"description": "Password of the bucket.",
"deprecation": {
"reason": "A bucket is no longer auto-configured.",
"level": "error"
}
},
{
"name": "spring.couchbase.env.bootstrap.http-direct-port",
"type": "java.lang.Integer",
"description": "Port for the HTTP bootstrap.",
"deprecation": {
"level": "error"
}
},
{
"name": "spring.couchbase.env.bootstrap.http-ssl-port",
"type": "java.lang.Integer",
"description": "Port for the HTTPS bootstrap.",
"deprecation": {
"level": "error"
}
},
{
"name": "spring.couchbase.env.endpoints.key-value",
"type": "java.lang.Integer",
"description": "Number of sockets per node against the key/value service.",
"deprecation": {
"level": "error"
}
},
{
"name": "spring.couchbase.env.endpoints.query",
"type": "java.lang.Integer",
"description": "Number of sockets per node against the query (N1QL) service.",
"deprecation": {
"level": "error"
}
},
{
"name": "spring.couchbase.env.endpoints.queryservice.max-endpoints",
"type": "java.lang.Integer",
"description": "Maximum number of sockets per node.",
"deprecation": {
"replacement": "spring.couchbase.env.io.max-endpoints",
"level": "error"
}
},
{
"name": "spring.couchbase.env.endpoints.queryservice.min-endpoints",
"type": "java.lang.Integer",
"description": "Minimum number of sockets per node.",
"deprecation": {
"replacement": "spring.couchbase.env.io.min-endpoints",
"level": "error"
}
},
{
"name": "spring.couchbase.env.endpoints.view",
"type": "java.lang.Integer",
"description": "Number of sockets per node against the view service.",
"deprecation": {
"level": "error"
}
},
{
"name": "spring.couchbase.env.endpoints.viewservice.max-endpoints",
"type": "java.lang.Integer",
"description": "Maximum number of sockets per node.",
"deprecation": {
"replacement": "spring.couchbase.env.io.max-endpoints",
"level": "error"
}
},
{
"name": "spring.couchbase.env.endpoints.viewservice.min-endpoints",
"type": "java.lang.Integer",
"description": "Minimum number of sockets per node.",
"deprecation": {
"replacement": "spring.couchbase.env.io.min-endpoints",
"level": "error"
}
},
{
"name": "spring.couchbase.env.ssl.key-store",
"type": "java.lang.String",
"description": "Path to the JVM key store that holds the certificates.",
"deprecation": {
"replacement": "spring.couchbase.env.ssl.bundle",
"level": "error",
"since": "3.1.0"
}
},
{
"name": "spring.couchbase.env.ssl.key-store-password",
"type": "java.lang.String",
"description": "Password used to access the key store.",
"deprecation": {
"replacement": "spring.couchbase.env.ssl.bundle",
"level": "error",
"since": "3.1.0"
}
},
{
"name": "spring.couchbase.env.timeouts.socket-connect",
"type": "java.time.Duration",
"description": "Socket connect connections timeout.",
"deprecation": {
"level": "error"
}
}
]
}

View File

@@ -0,0 +1 @@
org.springframework.boot.couchbase.autoconfigure.CouchbaseAutoConfiguration

View File

@@ -0,0 +1,329 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed 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 org.springframework.boot.couchbase.autoconfigure;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import com.couchbase.client.core.env.Authenticator;
import com.couchbase.client.core.env.CertificateAuthenticator;
import com.couchbase.client.core.env.IoConfig;
import com.couchbase.client.core.env.PasswordAuthenticator;
import com.couchbase.client.core.env.SecurityConfig;
import com.couchbase.client.core.env.TimeoutConfig;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.codec.JacksonJsonSerializer;
import com.couchbase.client.java.codec.JsonSerializer;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.json.JsonValueModule;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.couchbase.autoconfigure.CouchbaseAutoConfiguration.PropertiesCouchbaseConnectionDetails;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
import org.springframework.boot.ssl.NoSuchSslBundleException;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link CouchbaseAutoConfiguration}.
*
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
*/
class CouchbaseAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CouchbaseAutoConfiguration.class, SslAutoConfiguration.class));
@Test
void connectionStringIsRequired() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ClusterEnvironment.class)
.doesNotHaveBean(Authenticator.class)
.doesNotHaveBean(Cluster.class));
}
@Test
void definesPropertiesBasedConnectionDetailsByDefault() {
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
.withPropertyValues("spring.couchbase.connection-string=localhost")
.run((context) -> assertThat(context).hasSingleBean(PropertiesCouchbaseConnectionDetails.class));
}
@Test
void shouldUseCustomConnectionDetailsWhenDefined() {
this.contextRunner.withBean(CouchbaseConnectionDetails.class, this::couchbaseConnectionDetails)
.run((context) -> {
assertThat(context).hasSingleBean(ClusterEnvironment.class)
.hasSingleBean(Cluster.class)
.hasSingleBean(PasswordAuthenticator.class)
.hasSingleBean(CouchbaseConnectionDetails.class)
.doesNotHaveBean(PropertiesCouchbaseConnectionDetails.class);
Cluster cluster = context.getBean(Cluster.class);
assertThat(cluster.core()).extracting("connectionString.hosts")
.asInstanceOf(InstanceOfAssertFactories.LIST)
.extractingResultOf("host")
.containsExactly("couchbase.example.com");
});
}
@Test
void connectionStringCreateEnvironmentAndCluster() {
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
.withPropertyValues("spring.couchbase.connection-string=localhost")
.run((context) -> {
assertThat(context).hasSingleBean(ClusterEnvironment.class)
.hasSingleBean(Authenticator.class)
.hasSingleBean(Cluster.class);
assertThat(context).doesNotHaveBean("couchbaseAuthenticator");
assertThat(context.getBean(Cluster.class))
.isSameAs(context.getBean(CouchbaseTestConfiguration.class).couchbaseCluster());
});
}
@Test
void connectionDetailsOverridesProperties() {
this.contextRunner.withBean(CouchbaseConnectionDetails.class, this::couchbaseConnectionDetails)
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.username=a-user",
"spring.couchbase.password=a-password")
.run((context) -> {
assertThat(context).hasSingleBean(ClusterEnvironment.class)
.hasSingleBean(PasswordAuthenticator.class)
.hasSingleBean(Cluster.class);
Cluster cluster = context.getBean(Cluster.class);
assertThat(cluster.core()).extracting("connectionString.hosts")
.asInstanceOf(InstanceOfAssertFactories.LIST)
.extractingResultOf("host")
.containsExactly("couchbase.example.com");
});
}
@Test
void whenObjectMapperBeanIsDefinedThenClusterEnvironmentObjectMapperIsDerivedFromIt() {
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class))
.withPropertyValues("spring.couchbase.connection-string=localhost")
.run((context) -> {
ClusterEnvironment env = context.getBean(ClusterEnvironment.class);
Set<Object> expectedModuleIds = new HashSet<>(
context.getBean(ObjectMapper.class).getRegisteredModuleIds());
expectedModuleIds.add(new JsonValueModule().getTypeId());
JsonSerializer serializer = env.jsonSerializer();
assertThat(serializer).extracting("wrapped")
.isInstanceOf(JacksonJsonSerializer.class)
.extracting("mapper", as(InstanceOfAssertFactories.type(ObjectMapper.class)))
.extracting(ObjectMapper::getRegisteredModuleIds)
.isEqualTo(expectedModuleIds);
});
}
@Test
void customizeJsonSerializer() {
JsonSerializer customJsonSerializer = mock(JsonSerializer.class);
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class))
.withBean(ClusterEnvironmentBuilderCustomizer.class,
() -> (builder) -> builder.jsonSerializer(customJsonSerializer))
.withPropertyValues("spring.couchbase.connection-string=localhost")
.run((context) -> {
ClusterEnvironment env = context.getBean(ClusterEnvironment.class);
JsonSerializer serializer = env.jsonSerializer();
assertThat(serializer).extracting("wrapped").isSameAs(customJsonSerializer);
});
}
@Test
void customizeEnvIo() {
testClusterEnvironment((env) -> {
IoConfig ioConfig = env.ioConfig();
assertThat(ioConfig.numKvConnections()).isEqualTo(2);
assertThat(ioConfig.maxHttpConnections()).isEqualTo(5);
assertThat(ioConfig.idleHttpConnectionTimeout()).isEqualTo(Duration.ofSeconds(3));
}, "spring.couchbase.env.io.min-endpoints=2", "spring.couchbase.env.io.max-endpoints=5",
"spring.couchbase.env.io.idle-http-connection-timeout=3s");
}
@Test
void customizeEnvTimeouts() {
testClusterEnvironment((env) -> {
TimeoutConfig timeoutConfig = env.timeoutConfig();
assertThat(timeoutConfig.connectTimeout()).isEqualTo(Duration.ofSeconds(1));
assertThat(timeoutConfig.disconnectTimeout()).isEqualTo(Duration.ofSeconds(2));
assertThat(timeoutConfig.kvTimeout()).isEqualTo(Duration.ofMillis(500));
assertThat(timeoutConfig.kvDurableTimeout()).isEqualTo(Duration.ofMillis(750));
assertThat(timeoutConfig.queryTimeout()).isEqualTo(Duration.ofSeconds(3));
assertThat(timeoutConfig.viewTimeout()).isEqualTo(Duration.ofSeconds(4));
assertThat(timeoutConfig.searchTimeout()).isEqualTo(Duration.ofSeconds(5));
assertThat(timeoutConfig.analyticsTimeout()).isEqualTo(Duration.ofSeconds(6));
assertThat(timeoutConfig.managementTimeout()).isEqualTo(Duration.ofSeconds(7));
}, "spring.couchbase.env.timeouts.connect=1s", "spring.couchbase.env.timeouts.disconnect=2s",
"spring.couchbase.env.timeouts.key-value=500ms",
"spring.couchbase.env.timeouts.key-value-durable=750ms", "spring.couchbase.env.timeouts.query=3s",
"spring.couchbase.env.timeouts.view=4s", "spring.couchbase.env.timeouts.search=5s",
"spring.couchbase.env.timeouts.analytics=6s", "spring.couchbase.env.timeouts.management=7s");
}
@Test
void enableSsl() {
testClusterEnvironment((env) -> {
SecurityConfig securityConfig = env.securityConfig();
assertThat(securityConfig.tlsEnabled()).isTrue();
assertThat(securityConfig.trustManagerFactory()).isNotNull();
}, "spring.couchbase.env.ssl.enabled=true");
}
@Test
@WithPackageResources("test.jks")
void enableSslWithBundle() {
testClusterEnvironment((env) -> {
SecurityConfig securityConfig = env.securityConfig();
assertThat(securityConfig.tlsEnabled()).isTrue();
assertThat(securityConfig.trustManagerFactory()).isNotNull();
}, "spring.ssl.bundle.jks.test-bundle.truststore.location=classpath:test.jks",
"spring.ssl.bundle.jks.test-bundle.truststore.password=secret",
"spring.couchbase.env.ssl.bundle=test-bundle");
}
@Test
void enableSslWithInvalidBundle() {
this.contextRunner
.withPropertyValues("spring.couchbase.connection-string=localhost",
"spring.couchbase.env.ssl.bundle=test-bundle")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).rootCause()
.isInstanceOf(NoSuchSslBundleException.class)
.hasMessageContaining("test-bundle");
});
}
@Test
void disableSslEvenWithBundle() {
testClusterEnvironment((env) -> {
SecurityConfig securityConfig = env.securityConfig();
assertThat(securityConfig.tlsEnabled()).isFalse();
assertThat(securityConfig.trustManagerFactory()).isNull();
}, "spring.couchbase.env.ssl.enabled=false", "spring.couchbase.env.ssl.bundle=test-bundle");
}
private void testClusterEnvironment(Consumer<ClusterEnvironment> environmentConsumer, String... environment) {
this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class)
.withPropertyValues("spring.couchbase.connection-string=localhost")
.withPropertyValues(environment)
.run((context) -> environmentConsumer.accept(context.getBean(ClusterEnvironment.class)));
}
@Test
void customizeEnvWithCustomCouchbaseConfiguration() {
this.contextRunner
.withUserConfiguration(CouchbaseTestConfiguration.class, ClusterEnvironmentCustomizerConfiguration.class)
.withPropertyValues("spring.couchbase.connection-string=localhost",
"spring.couchbase.env.timeouts.connect=100")
.run((context) -> {
assertThat(context).hasSingleBean(ClusterEnvironment.class);
ClusterEnvironment env = context.getBean(ClusterEnvironment.class);
assertThat(env.timeoutConfig().kvTimeout()).isEqualTo(Duration.ofSeconds(5));
assertThat(env.timeoutConfig().connectTimeout()).isEqualTo(Duration.ofSeconds(2));
});
}
@Test
void passwordAuthenticationWithUsernameAndPassword() {
this.contextRunner
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.username=user",
"spring.couchbase.password=secret")
.run((context) -> assertThat(context).hasSingleBean(PasswordAuthenticator.class));
}
@Test
@WithPackageResources({ "key.crt", "key.pem" })
void certificateAuthenticationWithPemPrivateKeyAndCertificate() {
this.contextRunner
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.env.ssl.enabled=true",
"spring.couchbase.authentication.pem.private-key=classpath:key.pem",
"spring.couchbase.authentication.pem.certificates=classpath:key.crt")
.run((context) -> assertThat(context).hasSingleBean(CertificateAuthenticator.class));
}
@Test
@WithPackageResources("keystore.jks")
void certificateAuthenticationWithJavaKeyStore() {
this.contextRunner
.withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.env.ssl.enabled=true",
"spring.couchbase.authentication.jks.location=classpath:keystore.jks",
"spring.couchbase.authentication.jks.password=secret")
.run((context) -> assertThat(context).hasSingleBean(CertificateAuthenticator.class));
}
@Test
void failsWithMissingAuthentication() {
this.contextRunner.withPropertyValues("spring.couchbase.connection-string=localhost").run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure()
.hasMessageContaining("Couchbase authentication requires username and password, or certificates");
});
}
private CouchbaseConnectionDetails couchbaseConnectionDetails() {
return new CouchbaseConnectionDetails() {
@Override
public String getConnectionString() {
return "couchbase.example.com";
}
@Override
public String getUsername() {
return "user-1";
}
@Override
public String getPassword() {
return "password-1";
}
};
}
@Configuration(proxyBeanMethods = false)
static class ClusterEnvironmentCustomizerConfiguration {
@Bean
ClusterEnvironmentBuilderCustomizer clusterEnvironmentBuilderCustomizer() {
return (builder) -> builder.timeoutConfig()
.kvTimeout(Duration.ofSeconds(5))
.connectTimeout(Duration.ofSeconds(2));
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed 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 org.springframework.boot.couchbase.autoconfigure;
import com.couchbase.client.core.env.IoConfig;
import com.couchbase.client.core.env.TimeoutConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Io;
import org.springframework.boot.couchbase.autoconfigure.CouchbaseProperties.Timeouts;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CouchbaseProperties}.
*
* @author Stephane Nicoll
*/
class CouchbasePropertiesTests {
@Test
void ioHaveConsistentDefaults() {
Io io = new CouchbaseProperties().getEnv().getIo();
assertThat(io.getMinEndpoints()).isOne();
assertThat(io.getMaxEndpoints()).isEqualTo(IoConfig.DEFAULT_MAX_HTTP_CONNECTIONS);
assertThat(io.getIdleHttpConnectionTimeout()).isEqualTo(IoConfig.DEFAULT_IDLE_HTTP_CONNECTION_TIMEOUT);
}
@Test
void timeoutsHaveConsistentDefaults() {
Timeouts timeouts = new CouchbaseProperties().getEnv().getTimeouts();
assertThat(timeouts.getConnect()).isEqualTo(TimeoutConfig.DEFAULT_CONNECT_TIMEOUT);
assertThat(timeouts.getDisconnect()).isEqualTo(TimeoutConfig.DEFAULT_DISCONNECT_TIMEOUT);
assertThat(timeouts.getKeyValue()).isEqualTo(TimeoutConfig.DEFAULT_KV_TIMEOUT);
assertThat(timeouts.getKeyValueDurable()).isEqualTo(TimeoutConfig.DEFAULT_KV_DURABLE_TIMEOUT);
assertThat(timeouts.getQuery()).isEqualTo(TimeoutConfig.DEFAULT_QUERY_TIMEOUT);
assertThat(timeouts.getView()).isEqualTo(TimeoutConfig.DEFAULT_VIEW_TIMEOUT);
assertThat(timeouts.getSearch()).isEqualTo(TimeoutConfig.DEFAULT_SEARCH_TIMEOUT);
assertThat(timeouts.getAnalytics()).isEqualTo(TimeoutConfig.DEFAULT_ANALYTICS_TIMEOUT);
assertThat(timeouts.getManagement()).isEqualTo(TimeoutConfig.DEFAULT_MANAGEMENT_TIMEOUT);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed 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 org.springframework.boot.couchbase.autoconfigure;
import com.couchbase.client.core.env.Authenticator;
import com.couchbase.client.java.Cluster;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.mockito.Mockito.mock;
/**
* Test configuration for couchbase that mocks access.
*
* @author Stephane Nicoll
* @author Scott Frederick
*/
@Configuration(proxyBeanMethods = false)
class CouchbaseTestConfiguration {
private final Cluster cluster = mock(Cluster.class);
private final Authenticator authenticator = mock(Authenticator.class);
@Bean
Cluster couchbaseCluster() {
return this.cluster;
}
@Bean
Authenticator couchbaseAuth() {
return this.authenticator;
}
}

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDJjCCAhECFFjLlXVdTxDdLlCifzrA0dTHHJ2mMA0GCSqGSIb3DQEBCwUAME8x
CzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl
ZmF1bHQgQ29tcGFueSBMdGQxCzAJBgNVBAMMAkNBMCAXDTIzMTAwNTA3Mjg1MFoY
DzIxMjMwOTExMDcyODUwWjBRMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVs
dCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMQ0wCwYDVQQDDARr
ZXkyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAspCMUdFGyKkgpMbW
+UwSg4fdKM4qLSH7voTdsdVM9aAvLvYjBQ4gpORxDZNfUz67R0Ua0/oJt9jD49Wp
qcq+tDOnp0dPtn2hFluV5PxM6d+MCSx/frPsfvyt9234okLL1zdLDNFYEbLhSPjA
ku3vHw/OwlJOxCRwTkPqcElIV4+IvIbzAgSffyokzm/wKVKEhoT6NcfeU+6wCkTu
al1X8loJ+27N6jN13oGZfH7EveBqgR8rPs55+54S/OcVG/uqL9ggOGRJiIZ3jUBk
m5cN27wKkaNg/CQwa1UjcU4qshVpknHw1dpgJ2Gbs/yUphwpEZl/FTsZFcK1KCHD
rOp3PQIDAQABMA0GCSqGSIb3DQEBCwUAA4H/AAFmEq86broBFxs0cpImaM884PBT
bvJBSsFhsOg6mi4Gt01G/lPSj/ExNtH3G5bytCYAPaRxNx/dCs7uON3p86ta4zL8
2PxgyhX1oY/GG63ETwn5s3GKpRaGTNVDWvPIM9RX6+bvX/wOg8eYXVaQlG5XYadC
Ms9lWqHaM1C/iLGNmUTGcdbvhnmQDky2CwPNm+lXogSWbrsGpAmCkXJD1H+0Mx8I
wjDVtGLBwr/8oXI8WbhvISMnS9+dd7+GLm6mU+14Kswi5I7EmBmREvkswi2IVJ6M
GL7EY3qA6iqJWqsseYyLxiMr3nBT0SETphzoDanUQI1/jXQPrWIyjqvs
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCykIxR0UbIqSCk
xtb5TBKDh90oziotIfu+hN2x1Uz1oC8u9iMFDiCk5HENk19TPrtHRRrT+gm32MPj
1ampyr60M6enR0+2faEWW5Xk/Ezp34wJLH9+s+x+/K33bfiiQsvXN0sM0VgRsuFI
+MCS7e8fD87CUk7EJHBOQ+pwSUhXj4i8hvMCBJ9/KiTOb/ApUoSGhPo1x95T7rAK
RO5qXVfyWgn7bs3qM3XegZl8fsS94GqBHys+znn7nhL85xUb+6ov2CA4ZEmIhneN
QGSblw3bvAqRo2D8JDBrVSNxTiqyFWmScfDV2mAnYZuz/JSmHCkRmX8VOxkVwrUo
IcOs6nc9AgMBAAECggEAPN9dDolG1aIeYD3uzCa8Sv2WjdIWe7NRlEXMI9MgvL1i
SGKdVpxV0ZCU37llLkY85tNujWP4SyXIxdMxVxIoR9syJKsBSCd0sl//bgP6nmHY
Zco3HnTswu+VyLtDHuGhhtkxKwn0uXffKBaw44XcVhz38bPIaUI4zN2HPscks8BG
j2MEl0N8P/TVrTkhgdjfoRi73VAisrEe+1wCg74BT7cmR8fEr7iNFrv955sdPGdw
UTmx8U26++wbeYQs1ZE1713SYnRQuCUFs5GGjzOhNFi27zuhI6TafoVm9PO4j+ZC
JUKTyUTBUsRMvm9z1IoHdjM8yInAv2g0J1bAeCTY+wKBgQDuMNMbNVoiXRKsSUry
22T3W6HVLfLNKiYMNxsAkJjOiyyJcC+yg9BErn/haIHSafD2WmuWbW5ASViyl6fn
D8qMluTwEaSrTgHXWI4ahWyapDShDQYp1s4dB75Aa/LVcFCay54YEtyCPzCPlj1K
jz5OBV14NEVVA2cf59fIc/LXCwKBgQC/6m3TefUp5jnN/QUOx2OtZo8Y1pVrsuMB
AuTtb21Khxn/86ZpVzySzg79/DkSNf9/sZhzj0IkviWNP5S8iAAaFC1q08CYhdCX
d7tVnHlzpZmmoHUhG6dlJZayr1duZrURp2rP18+wIsKiFRImAyjc6yswVRpZgAiG
gOkHCB231wKBgGlwXZMWy/6YOtLfYvkcm5ZQDtSCkY+2j78qiZ53Y91SiHWSntqk
NQaiRGOw0n8lfJBhOG0PphV5InV0YtQLDnurtE59UOqwDmqYfddJpujRtaZxUIAm
4XjCW7rCzm0jWdscNbCscMaLWGDHffxKaqc5AsZaRTK73eOmysOmaCI/AoGAf/yd
RZ1dzJWHE0Kb7uE2LlvpLo1clLh1/ySo+1eGMV+sDS+2WSYedWEKSoO8o9JzE/ui
Sd7OI6bTcEFotdqVBs9SAp45IP6Mv5bPziZOMLvNnnv/4RaKKkBJId0hl7TTKHTY
HMg176ce2eznb4ZH6BzFbrQyoGFsThcGUPQurX0CgYBYtkDTp21TI1nuak7xpMIY
BJQpqF5ahBf/+QYWtL0f3ca9MO2++zv5/XXitvt48cY1bCHNrVvSHgRzwSrOorZA
5u7a5zyvfXjY3LY3k0VHddaVjU0mHsjx/1ux0wO2v8wQjOVZpT7XweB3WlUEGV7C
5T/p+rmGg5Y5dTKUVCyvbQ==
-----END PRIVATE KEY-----