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 value the configuration property value
|
||||
* @author Phillip Webb
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
record BundleContentProperty(String name, String value) {
|
||||
|
||||
|
@ -51,16 +52,17 @@ record BundleContentProperty(String name, String value) {
|
|||
}
|
||||
|
||||
Path toWatchPath() {
|
||||
return toPath();
|
||||
}
|
||||
|
||||
private Path toPath() {
|
||||
try {
|
||||
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());
|
||||
}
|
||||
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),
|
||||
ex);
|
||||
}
|
||||
|
|
|
@ -54,13 +54,14 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
|
|||
}
|
||||
|
||||
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) -> {
|
||||
Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties);
|
||||
try {
|
||||
registry.registerBundle(bundleName, bundleSupplier.get());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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<>();
|
||||
watched.add(new BundleContentProperty("keystore.location", properties.getKeystore().getLocation()));
|
||||
watched.add(new BundleContentProperty("truststore.location", properties.getTruststore().getLocation()));
|
||||
return watchedPaths(watched);
|
||||
watched.add(new BundleContentProperty("keystore.location", bundle.properties().getKeystore().getLocation()));
|
||||
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<>();
|
||||
watched.add(new BundleContentProperty("keystore.private-key", properties.getKeystore().getPrivateKey()));
|
||||
watched.add(new BundleContentProperty("keystore.certificate", properties.getKeystore().getCertificate()));
|
||||
watched.add(new BundleContentProperty("truststore.private-key", properties.getTruststore().getPrivateKey()));
|
||||
watched.add(new BundleContentProperty("truststore.certificate", properties.getTruststore().getCertificate()));
|
||||
return watchedPaths(watched);
|
||||
watched
|
||||
.add(new BundleContentProperty("keystore.private-key", bundle.properties().getKeystore().getPrivateKey()));
|
||||
watched
|
||||
.add(new BundleContentProperty("keystore.certificate", bundle.properties().getKeystore().getCertificate()));
|
||||
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) {
|
||||
return properties.stream()
|
||||
.filter(BundleContentProperty::hasValue)
|
||||
.map(BundleContentProperty::toWatchPath)
|
||||
.collect(Collectors.toSet());
|
||||
private Set<Path> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
|
||||
try {
|
||||
return properties.stream()
|
||||
.filter(BundleContentProperty::hasValue)
|
||||
.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.MissingR2dbcPoolDependencyFailureAnalyzer,\
|
||||
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
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
|
@ -83,4 +84,11 @@ class BundleContentPropertyTests {
|
|||
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