Skip to content

Commit

Permalink
Auto-configure health web components only if endpoint is exposed over…
Browse files Browse the repository at this point in the history
… HTTP

Fixes gh-28131

Co-authored-by: Phillip Webb <pwebb@vmware.com>
  • Loading branch information
mbhave and philwebb committed Oct 20, 2021
1 parent 42d21a8 commit b7521e2
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 188 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
import org.springframework.context.annotation.Conditional;
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. Matches enablement
* according to the endpoints specific {@link Environment} property, falling back to
* 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.<id>} or
Expand Down Expand Up @@ -112,4 +114,12 @@
*/
Class<?> endpoint() default Void.class;

/**
* Technologies to check the exposure of the endpoint on while considering it to be
* available.
* @return the technologies to check
* @since 2.6.0
*/
EndpointExposure[] exposure() default {};

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,34 @@

package org.springframework.boot.actuate.autoconfigure.endpoint.condition;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter.DefaultIncludes;
import org.springframework.boot.actuate.endpoint.EndpointId;
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.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.type.AnnotatedTypeMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;

/**
Expand All @@ -40,66 +54,140 @@
* @author Phillip Webb
* @see ConditionalOnAvailableEndpoint
*/
class OnAvailableEndpointCondition extends AbstractEndpointCondition {
class OnAvailableEndpointCondition extends SpringBootCondition {

private static final String JMX_ENABLED_KEY = "spring.jmx.enabled";

private static final Map<Environment, Set<Exposure>> exposuresCache = new ConcurrentReferenceHashMap<>();
private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default";

private static final Map<Environment, Set<ExposureFilter>> exposureFiltersCache = new ConcurrentReferenceHashMap<>();

private static final ConcurrentReferenceHashMap<Environment, Optional<Boolean>> enabledByDefaultCache = new ConcurrentReferenceHashMap<>();

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionOutcome enablementOutcome = getEnablementOutcome(context, metadata,
ConditionalOnAvailableEndpoint.class);
Environment environment = context.getEnvironment();
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation = metadata.getAnnotations()
.get(ConditionalOnAvailableEndpoint.class);
Class<?> target = getTarget(context, metadata, conditionAnnotation);
MergedAnnotation<Endpoint> endpointAnnotation = getEndpointAnnotation(target);
return getMatchOutcome(environment, conditionAnnotation, endpointAnnotation);
}

private Class<?> getTarget(ConditionContext context, AnnotatedTypeMetadata metadata,
MergedAnnotation<ConditionalOnAvailableEndpoint> condition) {
Class<?> target = condition.getClass("endpoint");
if (target != Void.class) {
return target;
}
Assert.state(metadata instanceof MethodMetadata && metadata.isAnnotated(Bean.class.getName()),
"EndpointCondition must be used on @Bean methods when the endpoint is not specified");
MethodMetadata methodMetadata = (MethodMetadata) metadata;
try {
return ClassUtils.forName(methodMetadata.getReturnTypeName(), context.getClassLoader());
}
catch (Throwable ex) {
throw new IllegalStateException("Failed to extract endpoint id for "
+ methodMetadata.getDeclaringClassName() + "." + methodMetadata.getMethodName(), ex);
}
}

protected MergedAnnotation<Endpoint> getEndpointAnnotation(Class<?> target) {
MergedAnnotations annotations = MergedAnnotations.from(target, SearchStrategy.TYPE_HIERARCHY);
MergedAnnotation<Endpoint> endpoint = annotations.get(Endpoint.class);
if (endpoint.isPresent()) {
return endpoint;
}
MergedAnnotation<EndpointExtension> extension = annotations.get(EndpointExtension.class);
Assert.state(extension.isPresent(), "No endpoint is specified and the return type of the @Bean method is "
+ "neither an @Endpoint, nor an @EndpointExtension");
return getEndpointAnnotation(extension.getClass("endpoint"));
}

private ConditionOutcome getMatchOutcome(Environment environment,
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation,
MergedAnnotation<Endpoint> endpointAnnotation) {
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;
}
ConditionMessage message = enablementOutcome.getConditionMessage();
Environment environment = context.getEnvironment();
if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) {
return new ConditionOutcome(true, message.andCondition(ConditionalOnAvailableEndpoint.class)
.because("application is running on Cloud Foundry"));
return ConditionOutcome.match(message.because("application is running on Cloud Foundry"));
}
EndpointId id = EndpointId.of(environment,
getEndpointAttributes(ConditionalOnAvailableEndpoint.class, context, metadata).getString("id"));
Set<Exposure> exposures = getExposures(environment);
for (Exposure exposure : exposures) {
if (exposure.isExposed(id)) {
return new ConditionOutcome(true,
message.andCondition(ConditionalOnAvailableEndpoint.class)
.because("marked as exposed by a 'management.endpoints." + exposure.getPrefix()
+ ".exposure' property"));
Set<EndpointExposure> exposuresToCheck = getExposuresToCheck(conditionAnnotation);
Set<ExposureFilter> 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 new ConditionOutcome(false, message.andCondition(ConditionalOnAvailableEndpoint.class)
.because("no 'management.endpoints' property marked it as exposed"));
return ConditionOutcome.noMatch(message.because("no 'management.endpoints' property marked it as exposed"));
}

private ConditionOutcome getEnablementOutcome(Environment environment,
MergedAnnotation<Endpoint> endpointAnnotation, EndpointId endpointId, ConditionMessage.Builder message) {
String key = "management.endpoint." + endpointId.toLowerCaseString() + ".enabled";
Boolean userDefinedEnabled = environment.getProperty(key, Boolean.class);
if (userDefinedEnabled != null) {
return new ConditionOutcome(userDefinedEnabled,
message.because("found property " + key + " with value " + userDefinedEnabled));
}
Boolean userDefinedDefault = isEnabledByDefault(environment);
if (userDefinedDefault != null) {
return new ConditionOutcome(userDefinedDefault, message.because(
"no property " + key + " found so using user defined default from " + ENABLED_BY_DEFAULT_KEY));
}
boolean endpointDefault = endpointAnnotation.getBoolean("enableByDefault");
return new ConditionOutcome(endpointDefault,
message.because("no property " + key + " found so using endpoint default of " + endpointDefault));
}

private Boolean isEnabledByDefault(Environment environment) {
Optional<Boolean> enabledByDefault = enabledByDefaultCache.get(environment);
if (enabledByDefault == null) {
enabledByDefault = Optional.ofNullable(environment.getProperty(ENABLED_BY_DEFAULT_KEY, Boolean.class));
enabledByDefaultCache.put(environment, enabledByDefault);
}
return enabledByDefault.orElse(null);
}

private Set<EndpointExposure> getExposuresToCheck(
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation) {
EndpointExposure[] exposure = conditionAnnotation.getEnumArray("exposure", EndpointExposure.class);
return (exposure.length == 0) ? EnumSet.allOf(EndpointExposure.class)
: new LinkedHashSet<>(Arrays.asList(exposure));
}

private Set<Exposure> getExposures(Environment environment) {
Set<Exposure> exposures = exposuresCache.get(environment);
if (exposures == null) {
exposures = new HashSet<>(2);
private Set<ExposureFilter> getExposureFilters(Environment environment) {
Set<ExposureFilter> exposureFilters = exposureFiltersCache.get(environment);
if (exposureFilters == null) {
exposureFilters = new HashSet<>(2);
if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) {
exposures.add(new Exposure(environment, "jmx", DefaultIncludes.JMX));
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.JMX));
}
exposures.add(new Exposure(environment, "web", DefaultIncludes.WEB));
exposuresCache.put(environment, exposures);
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.WEB));
exposureFiltersCache.put(environment, exposureFilters);
}
return exposures;
return exposureFilters;
}

static class Exposure extends IncludeExcludeEndpointFilter<ExposableEndpoint<?>> {
static final class ExposureFilter extends IncludeExcludeEndpointFilter<ExposableEndpoint<?>> {

private final String prefix;
private final EndpointExposure exposure;

@SuppressWarnings({ "rawtypes", "unchecked" })
Exposure(Environment environment, String prefix, DefaultIncludes defaultIncludes) {
super((Class) ExposableEndpoint.class, environment, "management.endpoints." + prefix + ".exposure",
defaultIncludes);
this.prefix = prefix;
@SuppressWarnings({ "unchecked", "rawtypes" })
private ExposureFilter(Environment environment, EndpointExposure exposure) {
super((Class) ExposableEndpoint.class, environment,
"management.endpoints." + exposure.name().toLowerCase() + ".exposure",
exposure.getDefaultIncludes());
this.exposure = exposure;
}

String getPrefix() {
return this.prefix;
EndpointExposure getExposure() {
return this.exposure;
}

boolean isExposed(EndpointId id) {
Expand Down
Loading

0 comments on commit b7521e2

Please sign in to comment.