From 73f71d5560a7a2285bd41fdd464e8ce271ed5ea1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 20 Aug 2024 07:31:01 -0700 Subject: [PATCH] Rework Cloud Foundry actuator support behind a pluggable abstraction Deprecate `EndpointExposure.CLOUD_FOUNDRY` and introduce an alternative implementation based on a pluggable abstraction. The new `EndpointExposureOutcomeContributor` interface may now be used to influence `@OnAvailableEndpointCondition` exposure results. Several infrastructure beans that previously used the condition have been refactored to always be registered, but tolerate missing endpoints. A new smoke test application has been added that demonstrates how the abstraction can be used by a third-party. Closes gh-41135 Co-authored-by: Phillip Webb --- .../CachesEndpointAutoConfiguration.java | 4 +- ...dryEndpointExposureOutcomeContributor.java | 57 ++++++++ ...ertiesReportEndpointAutoConfiguration.java | 4 +- .../ConditionalOnAvailableEndpoint.java | 25 ++-- .../EndpointExposureOutcomeContributor.java | 52 +++++++ .../OnAvailableEndpointCondition.java | 129 +++++++++++------- .../endpoint/expose/EndpointExposure.java | 5 +- ...ndpointManagementContextConfiguration.java | 20 +-- ...ndpointManagementContextConfiguration.java | 7 +- ...ndpointManagementContextConfiguration.java | 13 +- .../EnvironmentEndpointAutoConfiguration.java | 4 +- ...ointReactiveWebExtensionConfiguration.java | 9 +- ...althEndpointWebExtensionConfiguration.java | 13 +- .../QuartzEndpointAutoConfiguration.java | 4 +- .../sbom/SbomEndpointAutoConfiguration.java | 2 +- .../main/resources/META-INF/spring.factories | 4 + .../ConditionalOnAvailableEndpointTests.java | 5 +- .../HealthEndpointAutoConfigurationTests.java | 50 ++++++- ...lthEndpointPathsWebFluxHandlerMapping.java | 22 ++- ...althEndpointPathsWebMvcHandlerMapping.java | 22 ++- .../build.gradle | 13 ++ .../extension/MyExtensionConfiguration.java | 58 ++++++++ ...ionEndpointExposureOutcomeContributor.java | 47 +++++++ .../extension/MyExtensionEndpointFilter.java | 31 +++++ .../MyExtensionSecurityInterceptor.java | 38 ++++++ ...ExtensionWebMvcEndpointHandlerMapping.java | 76 +++++++++++ .../SampleActuatorExtensionApplication.java | 29 ++++ .../main/resources/META-INF/spring.factories | 2 + .../src/main/resources/application.properties | 4 + ...mpleActuatorExtensionApplicationTests.java | 71 ++++++++++ 30 files changed, 698 insertions(+), 122 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointExposureOutcomeContributor.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/EndpointExposureOutcomeContributor.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionConfiguration.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionEndpointExposureOutcomeContributor.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionEndpointFilter.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionSecurityInterceptor.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionWebMvcEndpointHandlerMapping.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/SampleActuatorExtensionApplication.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/resources/META-INF/spring.factories create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/resources/application.properties create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.java index a427adfdbc7..7cd5c01abb9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -52,7 +52,7 @@ public class CachesEndpointAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnBean(CachesEndpoint.class) - @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) + @ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB) public CachesEndpointWebExtension cachesEndpointWebExtension(CachesEndpoint cachesEndpoint) { return new CachesEndpointWebExtension(cachesEndpoint); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointExposureOutcomeContributor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointExposureOutcomeContributor.java new file mode 100644 index 00000000000..b0c06ad06f8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryEndpointExposureOutcomeContributor.java @@ -0,0 +1,57 @@ +/* + * 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.actuate.autoconfigure.cloudfoundry; + +import java.util.Set; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; +import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.core.env.Environment; + +/** + * {@link EndpointExposureOutcomeContributor} to expose {@link EndpointExposure#WEB web} + * endpoints for Cloud Foundry. + * + * @author Phillip Webb + */ +class CloudFoundryEndpointExposureOutcomeContributor implements EndpointExposureOutcomeContributor { + + private static final String PROPERTY = "management.endpoints.cloud-foundry.exposure"; + + private final IncludeExcludeEndpointFilter filter; + + CloudFoundryEndpointExposureOutcomeContributor(Environment environment) { + this.filter = (!CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) ? null + : new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, environment, PROPERTY, "*"); + } + + @Override + public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set exposures, + Builder message) { + if (exposures.contains(EndpointExposure.WEB) && this.filter != null && this.filter.match(endpointId)) { + return ConditionOutcome.match(message.because("marked as exposed by a '" + PROPERTY + "' property")); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java index 29c9ce665b7..848ebc0ae83 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -55,7 +55,7 @@ public class ConfigurationPropertiesReportEndpointAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnBean(ConfigurationPropertiesReportEndpoint.class) - @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) + @ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB) public ConfigurationPropertiesReportEndpointWebExtension configurationPropertiesReportEndpointWebExtension( ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint, ConfigurationPropertiesReportEndpointProperties properties) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.java index 2773446c3e1..6ab3f6f4a57 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * 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. @@ -31,15 +31,18 @@ import org.springframework.core.env.Environment; /** * {@link Conditional @Conditional} that checks whether an endpoint is available. An * endpoint is considered available if it is both enabled and exposed on the specified - * technologies. Matches enablement according to the endpoints specific - * {@link Environment} property, falling back to - * {@code management.endpoints.enabled-by-default} or failing that - * {@link Endpoint#enableByDefault()}. Matches exposure according to any of the - * {@code management.endpoints.web.exposure.} or - * {@code management.endpoints.jmx.exposure.} specific properties or failing that to - * whether the application runs on - * {@link org.springframework.boot.cloud.CloudPlatform#CLOUD_FOUNDRY}. Both those - * conditions should match for the endpoint to be considered available. + * technologies. + *

+ * Matches enablement according to the endpoints specific {@link Environment} property, + * falling back to {@code management.endpoints.enabled-by-default} or failing that + * {@link Endpoint#enableByDefault()}. + *

+ * Matches exposure according to any of the {@code management.endpoints.web.exposure.} + * or {@code management.endpoints.jmx.exposure.} specific properties or failing that + * to whether any {@link EndpointExposureOutcomeContributor} exposes the endpoint. + *

+ * Both enablement and exposure conditions should match for the endpoint to be considered + * available. *

* When placed on a {@code @Bean} method, the endpoint defaults to the return type of the * factory method: @@ -97,6 +100,8 @@ import org.springframework.core.env.Environment; * * @author Brian Clozel * @author Stephane Nicoll + * @author Andy Wilkinson + * @author Phillip Webb * @since 2.2.0 * @see Endpoint */ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/EndpointExposureOutcomeContributor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/EndpointExposureOutcomeContributor.java new file mode 100644 index 00000000000..4b74aa29ee3 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/EndpointExposureOutcomeContributor.java @@ -0,0 +1,52 @@ +/* + * 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.actuate.autoconfigure.endpoint.condition; + +import java.util.Set; + +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; +import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.core.env.Environment; + +/** + * Contributor loaded from the {@code spring.factories} file and used by + * {@link ConditionalOnAvailableEndpoint @ConditionalOnAvailableEndpoint} to determine if + * an endpoint is exposed. If any contributor returns a {@link ConditionOutcome#isMatch() + * matching} {@link ConditionOutcome} then the endpoint is considered exposed. + *

+ * Implementations may declare a constructor that accepts an {@link Environment} argument. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.4.0 + */ +public interface EndpointExposureOutcomeContributor { + + /** + * Return if the given endpoint is exposed for the given set of exposure technologies. + * @param endpointId the endpoint ID + * @param exposures the exposure technologies to check + * @param message the condition message builder + * @return a {@link ConditionOutcome#isMatch() matching} {@link ConditionOutcome} if + * the endpoint is exposed or {@code null} if the contributor should not apply + */ + ConditionOutcome getExposureOutcome(EndpointId endpointId, Set exposures, + ConditionMessage.Builder message); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java index a485aa2a4a1..2245d816120 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -17,9 +17,10 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.condition; import java.util.Arrays; +import java.util.Collection; import java.util.EnumSet; -import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -31,15 +32,17 @@ import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.cloud.CloudPlatform; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.env.Environment; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.util.Assert; @@ -52,6 +55,7 @@ import org.springframework.util.ConcurrentReferenceHashMap; * @author Brian Clozel * @author Stephane Nicoll * @author Phillip Webb + * @author Andy Wilkinson * @see ConditionalOnAvailableEndpoint */ class OnAvailableEndpointCondition extends SpringBootCondition { @@ -60,7 +64,7 @@ class OnAvailableEndpointCondition extends SpringBootCondition { private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default"; - private static final Map> exposureFiltersCache = new ConcurrentReferenceHashMap<>(); + private static final Map> exposureOutcomeContributorsCache = new ConcurrentReferenceHashMap<>(); private static final ConcurrentReferenceHashMap> enabledByDefaultCache = new ConcurrentReferenceHashMap<>(); @@ -110,18 +114,10 @@ class OnAvailableEndpointCondition extends SpringBootCondition { ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnAvailableEndpoint.class); EndpointId endpointId = EndpointId.of(environment, endpointAnnotation.getString("id")); ConditionOutcome enablementOutcome = getEnablementOutcome(environment, endpointAnnotation, endpointId, message); - if (!enablementOutcome.isMatch()) { - return enablementOutcome; - } - Set exposuresToCheck = getExposuresToCheck(conditionAnnotation); - Set exposureFilters = getExposureFilters(environment); - for (ExposureFilter exposureFilter : exposureFilters) { - if (exposuresToCheck.contains(exposureFilter.getExposure()) && exposureFilter.isExposed(endpointId)) { - return ConditionOutcome.match(message.because("marked as exposed by a 'management.endpoints." - + exposureFilter.getExposure().name().toLowerCase() + ".exposure' property")); - } - } - return ConditionOutcome.noMatch(message.because("no 'management.endpoints' property marked it as exposed")); + ConditionOutcome exposureOutcome = (!enablementOutcome.isMatch()) ? null + : getExposureOutcome(environment, conditionAnnotation, endpointAnnotation, endpointId, message); + return (exposureOutcome != null) ? exposureOutcome + : ConditionOutcome.noMatch(message.because("not enabled or exposed")); } private ConditionOutcome getEnablementOutcome(Environment environment, @@ -148,54 +144,83 @@ class OnAvailableEndpointCondition extends SpringBootCondition { return enabledByDefault.orElse(null); } - private Set getExposuresToCheck( - MergedAnnotation conditionAnnotation) { - EndpointExposure[] exposure = conditionAnnotation.getEnumArray("exposure", EndpointExposure.class); - return (exposure.length == 0) ? EnumSet.allOf(EndpointExposure.class) - : new LinkedHashSet<>(Arrays.asList(exposure)); - } - - private Set getExposureFilters(Environment environment) { - Set exposureFilters = exposureFiltersCache.get(environment); - if (exposureFilters == null) { - exposureFilters = new HashSet<>(2); - if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) { - exposureFilters.add(new ExposureFilter(environment, EndpointExposure.JMX)); + private ConditionOutcome getExposureOutcome(Environment environment, + MergedAnnotation conditionAnnotation, + MergedAnnotation endpointAnnotation, EndpointId endpointId, Builder message) { + Set exposures = getExposures(conditionAnnotation); + Set outcomeContributors = getExposureOutcomeContributors(environment); + for (EndpointExposureOutcomeContributor outcomeContributor : outcomeContributors) { + ConditionOutcome outcome = outcomeContributor.getExposureOutcome(endpointId, exposures, message); + if (outcome != null && outcome.isMatch()) { + return outcome; } - if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) { - exposureFilters.add(new ExposureFilter(environment, EndpointExposure.CLOUD_FOUNDRY)); - } - exposureFilters.add(new ExposureFilter(environment, EndpointExposure.WEB)); - exposureFiltersCache.put(environment, exposureFilters); } - return exposureFilters; + return null; } - static final class ExposureFilter extends IncludeExcludeEndpointFilter> { + private Set getExposures(MergedAnnotation conditionAnnotation) { + EndpointExposure[] exposures = conditionAnnotation.getEnumArray("exposure", EndpointExposure.class); + return replaceCloudFoundryExposure( + (exposures.length == 0) ? EnumSet.allOf(EndpointExposure.class) : Arrays.asList(exposures)); + } + + @SuppressWarnings("removal") + private Set replaceCloudFoundryExposure(Collection exposures) { + Set result = EnumSet.copyOf(exposures); + if (result.remove(EndpointExposure.CLOUD_FOUNDRY)) { + result.add(EndpointExposure.WEB); + } + return result; + } + + private Set getExposureOutcomeContributors(Environment environment) { + Set contributors = exposureOutcomeContributorsCache.get(environment); + if (contributors == null) { + contributors = new LinkedHashSet<>(); + contributors.add(new StandardExposureOutcomeContributor(environment, EndpointExposure.WEB)); + if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) { + contributors.add(new StandardExposureOutcomeContributor(environment, EndpointExposure.JMX)); + } + contributors.addAll(loadExposureOutcomeContributors(environment)); + exposureOutcomeContributorsCache.put(environment, contributors); + } + return contributors; + } + + private List loadExposureOutcomeContributors(Environment environment) { + ArgumentResolver argumentResolver = ArgumentResolver.of(Environment.class, environment); + return SpringFactoriesLoader.forDefaultResourceLocation() + .load(EndpointExposureOutcomeContributor.class, argumentResolver); + } + + /** + * Standard {@link EndpointExposureOutcomeContributor}. + */ + private static class StandardExposureOutcomeContributor implements EndpointExposureOutcomeContributor { private final EndpointExposure exposure; - @SuppressWarnings({ "unchecked", "rawtypes" }) - private ExposureFilter(Environment environment, EndpointExposure exposure) { - super((Class) ExposableEndpoint.class, environment, - "management.endpoints." + getCanonicalName(exposure) + ".exposure", exposure.getDefaultIncludes()); + private final String property; + + private final IncludeExcludeEndpointFilter filter; + + StandardExposureOutcomeContributor(Environment environment, EndpointExposure exposure) { this.exposure = exposure; + String name = exposure.name().toLowerCase().replace('_', '-'); + this.property = "management.endpoints." + name + ".exposure"; + this.filter = new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, environment, this.property, + exposure.getDefaultIncludes()); } - private static String getCanonicalName(EndpointExposure exposure) { - if (EndpointExposure.CLOUD_FOUNDRY.equals(exposure)) { - return "cloud-foundry"; + @Override + public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set exposures, + ConditionMessage.Builder message) { + if (exposures.contains(this.exposure) && this.filter.match(endpointId)) { + return ConditionOutcome + .match(message.because("marked as exposed by a '" + this.property + "' property")); } - return exposure.name().toLowerCase(); - } - - EndpointExposure getExposure() { - return this.exposure; - } - - boolean isExposed(EndpointId id) { - return super.match(id); + return null; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/EndpointExposure.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/EndpointExposure.java index 9b29b721299..967f72bdf63 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/EndpointExposure.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/EndpointExposure.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -37,7 +37,10 @@ public enum EndpointExposure { /** * Exposed on Cloud Foundry over `/cloudfoundryapplication`. * @since 2.6.4 + * @deprecated since 3.4.0 for removal in 3.6.0 in favor of using + * {@link EndpointExposure#WEB} */ + @Deprecated(since = "3.4.0", forRemoval = true) CLOUD_FOUNDRY("*"); private final String[] defaultIncludes; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index 3cc4da00068..587c6ba0f3a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -100,12 +100,12 @@ class JerseyWebEndpointManagementContextConfiguration { JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentPortAdditionalHealthEndpointPathsResourcesRegistrar( WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) { Collection webEndpoints = webEndpointsSupplier.getEndpoints(); - ExposableWebEndpoint health = webEndpoints.stream() + ExposableWebEndpoint healthEndpoint = webEndpoints.stream() .filter((endpoint) -> endpoint.getEndpointId().equals(HEALTH_ENDPOINT_ID)) .findFirst() - .orElseThrow( - () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HEALTH_ENDPOINT_ID))); - return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(health, healthEndpointGroups); + .orElse(null); + return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(healthEndpoint, + healthEndpointGroups); } @Bean @@ -180,19 +180,21 @@ class JerseyWebEndpointManagementContextConfiguration { class JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar implements ManagementContextResourceConfigCustomizer { - private final ExposableWebEndpoint endpoint; + private final ExposableWebEndpoint healthEndpoint; private final HealthEndpointGroups groups; - JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(ExposableWebEndpoint endpoint, + JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(ExposableWebEndpoint healthEndpoint, HealthEndpointGroups groups) { - this.endpoint = endpoint; + this.healthEndpoint = healthEndpoint; this.groups = groups; } @Override public void customize(ResourceConfig config) { - register(config); + if (this.healthEndpoint != null) { + register(config); + } } private void register(ResourceConfig config) { @@ -200,7 +202,7 @@ class JerseyWebEndpointManagementContextConfiguration { JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory( WebServerNamespace.MANAGEMENT, this.groups); Collection endpointResources = resourceFactory - .createEndpointResources(mapping, Collections.singletonList(this.endpoint)) + .createEndpointResources(mapping, Collections.singletonList(this.healthEndpoint)) .stream() .filter(Objects::nonNull) .toList(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index 1ba83a7a9b0..7e477275db8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -117,12 +117,11 @@ public class WebFluxEndpointManagementContextConfiguration { public AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpointWebFluxHandlerMapping( WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { Collection webEndpoints = webEndpointsSupplier.getEndpoints(); - ExposableWebEndpoint health = webEndpoints.stream() + ExposableWebEndpoint healthEndpoint = webEndpoints.stream() .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) .findFirst() - .orElseThrow( - () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID))); - return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), health, + .orElse(null); + return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), healthEndpoint, groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index ada988dd31c..dc5d8f159db 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -112,15 +112,18 @@ public class WebMvcEndpointManagementContextConfiguration { public AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpointWebMvcHandlerMapping( WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) { Collection webEndpoints = webEndpointsSupplier.getEndpoints(); - ExposableWebEndpoint health = webEndpoints.stream() - .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) + ExposableWebEndpoint healthEndpoint = webEndpoints.stream() + .filter(this::isHealthEndpoint) .findFirst() - .orElseThrow( - () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID))); - return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(health, + .orElse(null); + return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(healthEndpoint, groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT)); } + private boolean isHealthEndpoint(ExposableWebEndpoint endpoint) { + return endpoint.getEndpointId().equals(HealthEndpoint.ID); + } + @Bean @ConditionalOnMissingBean @SuppressWarnings("removal") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java index 42e8308833b..a89585216dd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -53,7 +53,7 @@ public class EnvironmentEndpointAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnBean(EnvironmentEndpoint.class) - @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) + @ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB) public EnvironmentEndpointWebExtension environmentEndpointWebExtension(EnvironmentEndpoint environmentEndpoint, EnvironmentEndpointProperties properties) { return new EnvironmentEndpointWebExtension(environmentEndpoint, properties.getShowValues(), diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java index 4a8d814ebd4..7bed528a650 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -45,8 +45,7 @@ import org.springframework.context.annotation.Configuration; */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.REACTIVE) -@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, - exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) +@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) class HealthEndpointReactiveWebExtensionConfiguration { @Bean @@ -60,7 +59,6 @@ class HealthEndpointReactiveWebExtensionConfiguration { } @Configuration(proxyBeanMethods = false) - @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) static class WebFluxAdditionalHealthEndpointPathsConfiguration { @Bean @@ -70,8 +68,7 @@ class HealthEndpointReactiveWebExtensionConfiguration { ExposableWebEndpoint health = webEndpoints.stream() .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) .findFirst() - .orElseThrow( - () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID))); + .orElse(null); return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), health, groups.getAllWithAdditionalPath(WebServerNamespace.SERVER)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java index a973b2f0fa4..9a917398ccd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -64,8 +64,7 @@ import org.springframework.web.servlet.DispatcherServlet; @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnBean(HealthEndpoint.class) -@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, - exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) +@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) class HealthEndpointWebExtensionConfiguration { @Bean @@ -81,12 +80,10 @@ class HealthEndpointWebExtensionConfiguration { return webEndpoints.stream() .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) .findFirst() - .orElseThrow( - () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID))); + .orElse(null); } @ConditionalOnBean(DispatcherServlet.class) - @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) static class MvcAdditionalHealthEndpointPathsConfiguration { @Bean @@ -102,7 +99,6 @@ class HealthEndpointWebExtensionConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ResourceConfig.class) @ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") - @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB) static class JerseyAdditionalHealthEndpointPathsConfiguration { @Bean @@ -163,7 +159,8 @@ class HealthEndpointWebExtensionConfiguration { JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory( WebServerNamespace.SERVER, this.groups); Collection endpointResources = resourceFactory - .createEndpointResources(mapping, Collections.singletonList(this.endpoint)) + .createEndpointResources(mapping, + (this.endpoint != null) ? Collections.singletonList(this.endpoint) : Collections.emptyList()) .stream() .filter(Objects::nonNull) .toList(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java index a97d2bcdd4a..d20c6cfa06f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -56,7 +56,7 @@ public class QuartzEndpointAutoConfiguration { @Bean @ConditionalOnBean(QuartzEndpoint.class) @ConditionalOnMissingBean - @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) + @ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB) public QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint, QuartzEndpointProperties properties) { return new QuartzEndpointWebExtension(endpoint, properties.getShowValues(), properties.getRoles()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/sbom/SbomEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/sbom/SbomEndpointAutoConfiguration.java index 4280b1cf7ef..caafaffb17f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/sbom/SbomEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/sbom/SbomEndpointAutoConfiguration.java @@ -55,7 +55,7 @@ public class SbomEndpointAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnBean(SbomEndpoint.class) - @ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) + @ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB) SbomEndpointWebExtension sbomEndpointWebExtension(SbomEndpoint sbomEndpoint) { return new SbomEndpointWebExtension(sbomEndpoint, this.properties); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index 619e3846a72..e6f6ca994b7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,3 +1,7 @@ +# Endpoint Exposure Outcome Contributors +org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor=\ +org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryEndpointExposureOutcomeContributor + # Failure Analyzers org.springframework.boot.diagnostics.FailureAnalyzer=\ org.springframework.boot.actuate.autoconfigure.health.NoSuchHealthContributorFailureAnalyzer,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java index c19c434daec..67747bf4573 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -374,8 +374,7 @@ class ConditionalOnAvailableEndpointTests { static class ExposureEndpointConfiguration { @Bean - @ConditionalOnAvailableEndpoint(endpoint = TestEndpoint.class, - exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY }) + @ConditionalOnAvailableEndpoint(endpoint = TestEndpoint.class, exposure = EndpointExposure.WEB) String unexposed() { return "unexposed"; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java index 6edda91c3d0..d1144387ac2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -26,6 +26,9 @@ import reactor.core.publisher.Mono; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration.HealthEndpointGroupMembershipValidator.NoSuchHealthContributorException; +import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointReactiveWebExtensionConfiguration.WebFluxAdditionalHealthEndpointPathsConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration.JerseyAdditionalHealthEndpointPathsConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration.MvcAdditionalHealthEndpointPathsConfiguration; import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; @@ -50,12 +53,16 @@ import org.springframework.boot.actuate.health.Status; import org.springframework.boot.actuate.health.StatusAggregator; import org.springframework.boot.actuate.health.SystemHealth; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.DispatcherServlet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -337,6 +344,47 @@ class HealthEndpointAutoConfigurationTests { })); } + @Test + void additionalHealthEndpointsPathsTolerateHealthEndpointThatIsNotWebExposed() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.exclude=*", + "management.endpoints.cloudfoundry.exposure.include=*", "spring.main.cloud-platform=cloud_foundry") + .run((context) -> { + assertThat(context).hasSingleBean(MvcAdditionalHealthEndpointPathsConfiguration.class); + assertThat(context).hasNotFailed(); + }); + } + + @Test + void additionalJerseyHealthEndpointsPathsTolerateHealthEndpointThatIsNotWebExposed() { + this.contextRunner + .withConfiguration( + AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(DispatcherServlet.class)) + .withPropertyValues("management.endpoints.web.exposure.exclude=*", + "management.endpoints.cloudfoundry.exposure.include=*", "spring.main.cloud-platform=cloud_foundry") + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .run((context) -> { + assertThat(context).hasSingleBean(JerseyAdditionalHealthEndpointPathsConfiguration.class); + assertThat(context).hasNotFailed(); + }); + } + + @Test + void additionalReactiveHealthEndpointsPathsTolerateHealthEndpointThatIsNotWebExposed() { + this.reactiveContextRunner + .withConfiguration( + AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.exclude=*", + "management.endpoints.cloudfoundry.exposure.include=*", "spring.main.cloud-platform=cloud_foundry") + .run((context) -> { + assertThat(context).hasSingleBean(WebFluxAdditionalHealthEndpointPathsConfiguration.class); + assertThat(context).hasNotFailed(); + }); + } + @Configuration(proxyBeanMethods = false) static class HealthIndicatorsConfiguration { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AdditionalHealthEndpointPathsWebFluxHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AdditionalHealthEndpointPathsWebFluxHandlerMapping.java index 46484261670..93f74288693 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AdditionalHealthEndpointPathsWebFluxHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AdditionalHealthEndpointPathsWebFluxHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * 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. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.endpoint.web.reactive; +import java.util.Collection; import java.util.Collections; import java.util.Set; @@ -41,21 +42,28 @@ public class AdditionalHealthEndpointPathsWebFluxHandlerMapping extends Abstract private final EndpointMapping endpointMapping; - private final ExposableWebEndpoint endpoint; + private final ExposableWebEndpoint healthEndpoint; private final Set groups; public AdditionalHealthEndpointPathsWebFluxHandlerMapping(EndpointMapping endpointMapping, - ExposableWebEndpoint endpoint, Set groups) { - super(endpointMapping, Collections.singletonList(endpoint), null, null, false); + ExposableWebEndpoint healthEndpoint, Set groups) { + super(endpointMapping, asList(healthEndpoint), null, null, false); this.endpointMapping = endpointMapping; this.groups = groups; - this.endpoint = endpoint; + this.healthEndpoint = healthEndpoint; + } + + private static Collection asList(ExposableWebEndpoint healthEndpoint) { + return (healthEndpoint != null) ? Collections.singletonList(healthEndpoint) : Collections.emptyList(); } @Override protected void initHandlerMethods() { - for (WebOperation operation : this.endpoint.getOperations()) { + if (this.healthEndpoint == null) { + return; + } + for (WebOperation operation : this.healthEndpoint.getOperations()) { WebOperationRequestPredicate predicate = operation.getRequestPredicate(); String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable(); if (matchAllRemainingPathSegmentsVariable != null) { @@ -64,7 +72,7 @@ public class AdditionalHealthEndpointPathsWebFluxHandlerMapping extends Abstract if (additionalPath != null) { RequestMappingInfo requestMappingInfo = getRequestMappingInfo(operation, additionalPath.getValue()); - registerReadMapping(requestMappingInfo, this.endpoint, operation); + registerReadMapping(requestMappingInfo, this.healthEndpoint, operation); } } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AdditionalHealthEndpointPathsWebMvcHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AdditionalHealthEndpointPathsWebMvcHandlerMapping.java index 48b717cb343..6b3573f47af 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AdditionalHealthEndpointPathsWebMvcHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AdditionalHealthEndpointPathsWebMvcHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * 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. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.endpoint.web.servlet; +import java.util.Collection; import java.util.Collections; import java.util.Set; @@ -36,27 +37,34 @@ import org.springframework.web.servlet.HandlerMapping; */ public class AdditionalHealthEndpointPathsWebMvcHandlerMapping extends AbstractWebMvcEndpointHandlerMapping { - private final ExposableWebEndpoint endpoint; + private final ExposableWebEndpoint healthEndpoint; private final Set groups; - public AdditionalHealthEndpointPathsWebMvcHandlerMapping(ExposableWebEndpoint endpoint, + public AdditionalHealthEndpointPathsWebMvcHandlerMapping(ExposableWebEndpoint healthEndpoint, Set groups) { - super(new EndpointMapping(""), Collections.singletonList(endpoint), null, false); - this.endpoint = endpoint; + super(new EndpointMapping(""), asList(healthEndpoint), null, false); + this.healthEndpoint = healthEndpoint; this.groups = groups; } + private static Collection asList(ExposableWebEndpoint healthEndpoint) { + return (healthEndpoint != null) ? Collections.singletonList(healthEndpoint) : Collections.emptyList(); + } + @Override protected void initHandlerMethods() { - for (WebOperation operation : this.endpoint.getOperations()) { + if (this.healthEndpoint == null) { + return; + } + for (WebOperation operation : this.healthEndpoint.getOperations()) { WebOperationRequestPredicate predicate = operation.getRequestPredicate(); String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable(); if (matchAllRemainingPathSegmentsVariable != null) { for (HealthEndpointGroup group : this.groups) { AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath(); if (additionalPath != null) { - registerMapping(this.endpoint, predicate, operation, additionalPath.getValue()); + registerMapping(this.healthEndpoint, predicate, operation, additionalPath.getValue()); } } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/build.gradle new file mode 100644 index 00000000000..3e138397df5 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot Actuator Extension smoke test" + +dependencies { + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionConfiguration.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionConfiguration.java new file mode 100644 index 00000000000..8fe5193658e --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionConfiguration.java @@ -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 smoketest.actuator.extension; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; +import org.springframework.boot.actuate.endpoint.EndpointFilter; +import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; +import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.web.cors.CorsConfiguration; + +@Configuration(proxyBeanMethods = false) +public class MyExtensionConfiguration { + + @Bean + public MyExtensionWebMvcEndpointHandlerMapping myWebMvcEndpointHandlerMapping( + WebEndpointsSupplier webEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, + ObjectProvider corsPropertiesProvider, WebEndpointProperties webEndpointProperties, + Environment environment, ApplicationContext applicationContext, ParameterValueMapper parameterMapper) { + CorsEndpointProperties corsProperties = corsPropertiesProvider.getIfAvailable(); + CorsConfiguration corsConfiguration = (corsProperties != null) ? corsProperties.toCorsConfiguration() : null; + List invokerAdvisors = Collections.emptyList(); + List> filters = Collections + .singletonList(new MyExtensionEndpointFilter(environment)); + WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(applicationContext, parameterMapper, + endpointMediaTypes, null, invokerAdvisors, filters); + Collection endpoints = discoverer.getEndpoints(); + return new MyExtensionWebMvcEndpointHandlerMapping(endpoints, endpointMediaTypes, corsConfiguration); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionEndpointExposureOutcomeContributor.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionEndpointExposureOutcomeContributor.java new file mode 100644 index 00000000000..01b9612799d --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionEndpointExposureOutcomeContributor.java @@ -0,0 +1,47 @@ +/* + * 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 smoketest.actuator.extension; + +import java.util.Set; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure; +import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.core.env.Environment; + +class MyExtensionEndpointExposureOutcomeContributor implements EndpointExposureOutcomeContributor { + + private final MyExtensionEndpointFilter filter; + + MyExtensionEndpointExposureOutcomeContributor(Environment environment) { + this.filter = new MyExtensionEndpointFilter(environment); + } + + @Override + public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set exposures, + Builder message) { + if (exposures.contains(EndpointExposure.WEB) && this.filter.match(endpointId)) { + return ConditionOutcome.match(message.because("marked as exposed by a my extension '" + + MyExtensionEndpointFilter.PROPERTY_PREFIX + "' property")); + } + return null; + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionEndpointFilter.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionEndpointFilter.java new file mode 100644 index 00000000000..c03e7a1277d --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionEndpointFilter.java @@ -0,0 +1,31 @@ +/* + * 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 smoketest.actuator.extension; + +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +import org.springframework.core.env.Environment; + +class MyExtensionEndpointFilter extends IncludeExcludeEndpointFilter { + + static final String PROPERTY_PREFIX = "management.endpoints.myextension.exposure"; + + MyExtensionEndpointFilter(Environment environment) { + super(ExposableWebEndpoint.class, environment, PROPERTY_PREFIX, "*"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionSecurityInterceptor.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionSecurityInterceptor.java new file mode 100644 index 00000000000..27144c1290d --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionSecurityInterceptor.java @@ -0,0 +1,38 @@ +/* + * 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 smoketest.actuator.extension; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.HandlerInterceptor; + +class MyExtensionSecurityInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String auth = request.getHeader("Authorization"); + if (!"Bearer secret".equals(auth)) { + response.sendError(HttpStatus.UNAUTHORIZED.value()); + return false; + } + return true; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionWebMvcEndpointHandlerMapping.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionWebMvcEndpointHandlerMapping.java new file mode 100644 index 00000000000..6a6bd229516 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/MyExtensionWebMvcEndpointHandlerMapping.java @@ -0,0 +1,76 @@ +/* + * 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 smoketest.actuator.extension; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; +import org.springframework.boot.actuate.endpoint.web.EndpointMapping; +import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +import org.springframework.boot.actuate.endpoint.web.Link; +import org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.cors.CorsConfiguration; + +class MyExtensionWebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerMapping { + + private static final String PATH = "/myextension"; + + private final EndpointLinksResolver linksResolver; + + MyExtensionWebMvcEndpointHandlerMapping(Collection endpoints, + EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) { + super(new EndpointMapping(PATH), endpoints, endpointMediaTypes, corsConfiguration, true); + this.linksResolver = new EndpointLinksResolver(endpoints, PATH); + setOrder(-100); + } + + @Override + protected LinksHandler getLinksHandler() { + return new WebMvcLinksHandler(); + } + + @Override + protected void extendInterceptors(List interceptors) { + super.extendInterceptors(interceptors); + interceptors.add(0, new MyExtensionSecurityInterceptor()); + } + + class WebMvcLinksHandler implements LinksHandler { + + @Override + @ResponseBody + public Map> links(HttpServletRequest request, HttpServletResponse response) { + return Collections.singletonMap("_links", MyExtensionWebMvcEndpointHandlerMapping.this.linksResolver + .resolveLinks(request.getRequestURL().toString())); + } + + @Override + public String toString() { + return "Actuator extension root web endpoint"; + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/SampleActuatorExtensionApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/SampleActuatorExtensionApplication.java new file mode 100644 index 00000000000..2593636d081 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/java/smoketest/actuator/extension/SampleActuatorExtensionApplication.java @@ -0,0 +1,29 @@ +/* + * 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 smoketest.actuator.extension; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(proxyBeanMethods = false) +public class SampleActuatorExtensionApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleActuatorExtensionApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/resources/META-INF/spring.factories b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..32f45f964dd --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor=\ +smoketest.actuator.extension.MyExtensionEndpointExposureOutcomeContributor diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/resources/application.properties new file mode 100644 index 00000000000..b4d44569f0d --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.jmx.enabled=false +management.endpoints.web.exposure.exclude=* +management.endpoints.myextension.exposure.include=health,beans,configprops + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java new file mode 100644 index 00000000000..a2f07a28e69 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-extension/src/test/java/smoketest/actuator/extension/SampleActuatorExtensionApplicationTests.java @@ -0,0 +1,71 @@ +/* + * 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 smoketest.actuator.extension; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.LocalHostUriTemplateHandler; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "server.error.include-message=always" }) +class SampleActuatorExtensionApplicationTests { + + @Autowired + private Environment environment; + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private RestTemplateBuilder restTemplateBuilder; + + @Test + @SuppressWarnings("rawtypes") + void healthActuatorIsNotExposed() { + ResponseEntity entity = this.restTemplate.getForEntity("/actuator/health", Map.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + @SuppressWarnings("rawtypes") + void healthExtensionWithAuthHeaderIsDenied() { + ResponseEntity entity = this.restTemplate.getForEntity("/myextension/health", Map.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + @SuppressWarnings("rawtypes") + void healthExtensionWithAuthHeader() { + TestRestTemplate restTemplate = new TestRestTemplate( + this.restTemplateBuilder.defaultHeader("Authorization", "Bearer secret")); + restTemplate.setUriTemplateHandler(new LocalHostUriTemplateHandler(this.environment)); + ResponseEntity entity = restTemplate.getForEntity("/myextension/health", Map.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + +}