commit
12cfb1fd2f
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2024 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.autoconfigure.ssl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when a bundle content location is not watchable.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
*/
|
||||||
|
class BundleContentNotWatchableException extends RuntimeException {
|
||||||
|
|
||||||
|
private final BundleContentProperty property;
|
||||||
|
|
||||||
|
BundleContentNotWatchableException(BundleContentProperty property) {
|
||||||
|
super("The content of '%s' is not watchable. Only 'file:' resources are watchable, but '%s' has been set"
|
||||||
|
.formatted(property.name(), property.value()));
|
||||||
|
this.property = property;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BundleContentNotWatchableException(String bundleName, BundleContentProperty property, Throwable cause) {
|
||||||
|
super("The content of '%s' from bundle '%s' is not watchable'. Only 'file:' resources are watchable, but '%s' has been set"
|
||||||
|
.formatted(property.name(), bundleName, property.value()), cause);
|
||||||
|
this.property = property;
|
||||||
|
}
|
||||||
|
|
||||||
|
BundleContentNotWatchableException withBundleName(String bundleName) {
|
||||||
|
return new BundleContentNotWatchableException(bundleName, this.property, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2024 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.autoconfigure.ssl;
|
||||||
|
|
||||||
|
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
|
||||||
|
import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link AbstractFailureAnalyzer} that performs analysis of non-watchable bundle
|
||||||
|
* content failures caused by {@link BundleContentNotWatchableException}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
*/
|
||||||
|
class BundleContentNotWatchableFailureAnalyzer extends AbstractFailureAnalyzer<BundleContentNotWatchableException> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FailureAnalysis analyze(Throwable rootFailure, BundleContentNotWatchableException cause) {
|
||||||
|
return new FailureAnalysis(cause.getMessage(), "Update your application to correct the invalid configuration:\n"
|
||||||
|
+ "Either use a watchable resource, or disable bundle reloading by setting reload-on-update = false on the bundle.",
|
||||||
|
cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import org.springframework.util.StringUtils;
|
||||||
* @param name the configuration property name (excluding any prefix)
|
* @param name the configuration property name (excluding any prefix)
|
||||||
* @param value the configuration property value
|
* @param value the configuration property value
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Moritz Halbritter
|
||||||
*/
|
*/
|
||||||
record BundleContentProperty(String name, String value) {
|
record BundleContentProperty(String name, String value) {
|
||||||
|
|
||||||
|
@ -51,16 +52,17 @@ record BundleContentProperty(String name, String value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Path toWatchPath() {
|
Path toWatchPath() {
|
||||||
return toPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path toPath() {
|
|
||||||
try {
|
try {
|
||||||
Resource resource = getResource();
|
Resource resource = getResource();
|
||||||
Assert.state(resource.isFile(), () -> "Value '%s' is not a file resource".formatted(this.value));
|
if (!resource.isFile()) {
|
||||||
|
throw new BundleContentNotWatchableException(this);
|
||||||
|
}
|
||||||
return Path.of(resource.getFile().getAbsolutePath());
|
return Path.of(resource.getFile().getAbsolutePath());
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
|
if (ex instanceof BundleContentNotWatchableException bundleContentNotWatchableException) {
|
||||||
|
throw bundleContentNotWatchableException;
|
||||||
|
}
|
||||||
throw new IllegalStateException("Unable to convert value of property '%s' to a path".formatted(this.name),
|
throw new IllegalStateException("Unable to convert value of property '%s' to a path".formatted(this.name),
|
||||||
ex);
|
ex);
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,13 +54,14 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
|
||||||
}
|
}
|
||||||
|
|
||||||
private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties,
|
private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties,
|
||||||
Function<P, SslBundle> bundleFactory, Function<P, Set<Path>> watchedPaths) {
|
Function<P, SslBundle> bundleFactory, Function<Bundle<P>, Set<Path>> watchedPaths) {
|
||||||
properties.forEach((bundleName, bundleProperties) -> {
|
properties.forEach((bundleName, bundleProperties) -> {
|
||||||
Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties);
|
Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties);
|
||||||
try {
|
try {
|
||||||
registry.registerBundle(bundleName, bundleSupplier.get());
|
registry.registerBundle(bundleName, bundleSupplier.get());
|
||||||
if (bundleProperties.isReloadOnUpdate()) {
|
if (bundleProperties.isReloadOnUpdate()) {
|
||||||
Supplier<Set<Path>> pathsSupplier = () -> watchedPaths.apply(bundleProperties);
|
Supplier<Set<Path>> pathsSupplier = () -> watchedPaths
|
||||||
|
.apply(new Bundle<>(bundleName, bundleProperties));
|
||||||
watchForUpdates(registry, bundleName, pathsSupplier, bundleSupplier);
|
watchForUpdates(registry, bundleName, pathsSupplier, bundleSupplier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,27 +81,40 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Path> watchedJksPaths(JksSslBundleProperties properties) {
|
private Set<Path> watchedJksPaths(Bundle<JksSslBundleProperties> bundle) {
|
||||||
List<BundleContentProperty> watched = new ArrayList<>();
|
List<BundleContentProperty> watched = new ArrayList<>();
|
||||||
watched.add(new BundleContentProperty("keystore.location", properties.getKeystore().getLocation()));
|
watched.add(new BundleContentProperty("keystore.location", bundle.properties().getKeystore().getLocation()));
|
||||||
watched.add(new BundleContentProperty("truststore.location", properties.getTruststore().getLocation()));
|
watched
|
||||||
return watchedPaths(watched);
|
.add(new BundleContentProperty("truststore.location", bundle.properties().getTruststore().getLocation()));
|
||||||
|
return watchedPaths(bundle.name(), watched);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Path> watchedPemPaths(PemSslBundleProperties properties) {
|
private Set<Path> watchedPemPaths(Bundle<PemSslBundleProperties> bundle) {
|
||||||
List<BundleContentProperty> watched = new ArrayList<>();
|
List<BundleContentProperty> watched = new ArrayList<>();
|
||||||
watched.add(new BundleContentProperty("keystore.private-key", properties.getKeystore().getPrivateKey()));
|
watched
|
||||||
watched.add(new BundleContentProperty("keystore.certificate", properties.getKeystore().getCertificate()));
|
.add(new BundleContentProperty("keystore.private-key", bundle.properties().getKeystore().getPrivateKey()));
|
||||||
watched.add(new BundleContentProperty("truststore.private-key", properties.getTruststore().getPrivateKey()));
|
watched
|
||||||
watched.add(new BundleContentProperty("truststore.certificate", properties.getTruststore().getCertificate()));
|
.add(new BundleContentProperty("keystore.certificate", bundle.properties().getKeystore().getCertificate()));
|
||||||
return watchedPaths(watched);
|
watched.add(new BundleContentProperty("truststore.private-key",
|
||||||
|
bundle.properties().getTruststore().getPrivateKey()));
|
||||||
|
watched.add(new BundleContentProperty("truststore.certificate",
|
||||||
|
bundle.properties().getTruststore().getCertificate()));
|
||||||
|
return watchedPaths(bundle.name(), watched);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Path> watchedPaths(List<BundleContentProperty> properties) {
|
private Set<Path> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
|
||||||
return properties.stream()
|
try {
|
||||||
.filter(BundleContentProperty::hasValue)
|
return properties.stream()
|
||||||
.map(BundleContentProperty::toWatchPath)
|
.filter(BundleContentProperty::hasValue)
|
||||||
.collect(Collectors.toSet());
|
.map(BundleContentProperty::toWatchPath)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
catch (BundleContentNotWatchableException ex) {
|
||||||
|
throw ex.withBundleName(bundleName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Bundle<P>(String name, P properties) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,8 @@ org.springframework.boot.autoconfigure.jooq.NoDslContextBeanFailureAnalyzer,\
|
||||||
org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
|
org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
|
||||||
org.springframework.boot.autoconfigure.r2dbc.MissingR2dbcPoolDependencyFailureAnalyzer,\
|
org.springframework.boot.autoconfigure.r2dbc.MissingR2dbcPoolDependencyFailureAnalyzer,\
|
||||||
org.springframework.boot.autoconfigure.r2dbc.MultipleConnectionPoolConfigurationsFailureAnalyzer,\
|
org.springframework.boot.autoconfigure.r2dbc.MultipleConnectionPoolConfigurationsFailureAnalyzer,\
|
||||||
org.springframework.boot.autoconfigure.r2dbc.NoConnectionFactoryBeanFailureAnalyzer
|
org.springframework.boot.autoconfigure.r2dbc.NoConnectionFactoryBeanFailureAnalyzer,\
|
||||||
|
org.springframework.boot.autoconfigure.ssl.BundleContentNotWatchableFailureAnalyzer
|
||||||
|
|
||||||
# Template Availability Providers
|
# Template Availability Providers
|
||||||
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
|
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2024 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.autoconfigure.ssl;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.diagnostics.FailureAnalysis;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link BundleContentNotWatchableFailureAnalyzer}.
|
||||||
|
*
|
||||||
|
* @author Moritz Halbritter
|
||||||
|
*/
|
||||||
|
class BundleContentNotWatchableFailureAnalyzerTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAnalyze() {
|
||||||
|
FailureAnalysis failureAnalysis = performAnalysis(null);
|
||||||
|
assertThat(failureAnalysis.getDescription()).isEqualTo(
|
||||||
|
"The content of 'name' is not watchable. Only 'file:' resources are watchable, but 'classpath:resource.pem' has been set");
|
||||||
|
assertThat(failureAnalysis.getAction())
|
||||||
|
.isEqualTo("Update your application to correct the invalid configuration:\n"
|
||||||
|
+ "Either use a watchable resource, or disable bundle reloading by setting reload-on-update = false on the bundle.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAnalyzeWithBundle() {
|
||||||
|
FailureAnalysis failureAnalysis = performAnalysis("bundle-1");
|
||||||
|
assertThat(failureAnalysis.getDescription()).isEqualTo(
|
||||||
|
"The content of 'name' from bundle 'bundle-1' is not watchable'. Only 'file:' resources are watchable, but 'classpath:resource.pem' has been set");
|
||||||
|
}
|
||||||
|
|
||||||
|
private FailureAnalysis performAnalysis(String bundle) {
|
||||||
|
BundleContentNotWatchableException failure = new BundleContentNotWatchableException(
|
||||||
|
new BundleContentProperty("name", "classpath:resource.pem"));
|
||||||
|
if (bundle != null) {
|
||||||
|
failure = failure.withBundleName(bundle);
|
||||||
|
}
|
||||||
|
return new BundleContentNotWatchableFailureAnalyzer().analyze(failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import java.nio.file.Path;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -83,4 +84,11 @@ class BundleContentPropertyTests {
|
||||||
assertThat(property.toWatchPath()).isEqualTo(file);
|
assertThat(property.toWatchPath()).isEqualTo(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowBundleContentNotWatchableExceptionIfContentIsNotWatchable() {
|
||||||
|
BundleContentProperty property = new BundleContentProperty("name", "https://example.com/");
|
||||||
|
assertThatExceptionOfType(BundleContentNotWatchableException.class).isThrownBy(property::toWatchPath)
|
||||||
|
.withMessageContaining("Only 'file:' resources are watchable");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue