Merge branch '3.2.x'

Closes gh-40560
This commit is contained in:
Moritz Halbritter 2024-04-29 10:04:32 +02:00
commit 12cfb1fd2f
7 changed files with 187 additions and 23 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
} }

View File

@ -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) {
} }
} }

View File

@ -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=\

View File

@ -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);
}
}

View File

@ -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");
}
} }