diff --git a/docs/modules/ROOT/pages/server/push-notifications-and-bus.adoc b/docs/modules/ROOT/pages/server/push-notifications-and-bus.adoc index 281046437..9c032c651 100644 --- a/docs/modules/ROOT/pages/server/push-notifications-and-bus.adoc +++ b/docs/modules/ROOT/pages/server/push-notifications-and-bus.adoc @@ -16,6 +16,43 @@ The default configuration works out of the box with Github, Gitlab, Gitea, Gitee In addition to the JSON notifications from Github, Gitlab, Gitee, or Bitbucket, you can trigger a change notification by POSTing to `/monitor` with form-encoded body parameters in the pattern of `path=\{application}`. Doing so broadcasts to applications matching the `\{application}` pattern (which can contain wildcards). +[[ignored-monitor-paths]] +== Ignoring Paths + +You can configure the Config Server to ignore specific paths when processing webhook notifications by setting `spring.cloud.config.server.monitor.ignored-paths` to a list of regular expressions. + +Ignored paths are excluded before the Config Server determines which applications should be refreshed. This can be useful when a repository contains files that should not trigger configuration refreshes. + +For example: + +[source,yaml] +---- +spring: + cloud: + config: + server: + monitor: + ignored-paths: + - "docker-compose/.*" + - "docs/.*" +---- + +Each regular expression is matched against the complete webhook path. A path is ignored when it matches any configured expression. + +By default, the Config Server processes at most 20 paths from a single notification. You can change this limit with `spring.cloud.config.server.monitor.max-paths`. If a notification contains more paths than the configured maximum, the remaining paths are not processed and a warning is logged. + +For example: + +[source,yaml] +---- +spring: + cloud: + config: + server: + monitor: + max-paths: 50 +---- + NOTE: The `RefreshRemoteApplicationEvent` is transmitted only if the `spring-cloud-bus` is activated in both the Config Server and in the client application. NOTE: The default configuration also detects filesystem changes in local git repositories. In that case, the webhook is not used. However, as soon as you edit a config file, a refresh is broadcast. diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java index b40d66404..a8f88609e 100644 --- a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfiguration.java @@ -75,7 +75,8 @@ protected static class BusPropertyPathConfiguration { public PropertyPathEndpoint propertyPathEndpoint(BusProperties busProperties, MonitorConfigurationProperties monitorProperties) { return new PropertyPathEndpoint(new CompositePropertyPathNotificationExtractor(this.extractors), - busProperties.getId(), monitorProperties.getMaxDashes()); + busProperties.getId(), monitorProperties.getMaxDashes(), monitorProperties.getMaxPaths(), + monitorProperties.getIgnoredPaths()); } // TODO: With the current implementation bus can't be disabled @@ -85,7 +86,8 @@ public PropertyPathEndpoint noBusBeanPropertyPathEndpoint( @Value("${spring.cloud.bus.id:application}") String id, MonitorConfigurationProperties monitorProperties) { return new PropertyPathEndpoint(new CompositePropertyPathNotificationExtractor(this.extractors), id, - monitorProperties.getMaxDashes()); + monitorProperties.getMaxDashes(), monitorProperties.getMaxPaths(), + monitorProperties.getIgnoredPaths()); } } @@ -99,7 +101,8 @@ public PropertyPathEndpoint noBusPropertyPathEndpoint(@Value("${spring.cloud.bus @Autowired(required = false) List extractors, MonitorConfigurationProperties monitorProperties) { return new PropertyPathEndpoint(new CompositePropertyPathNotificationExtractor(extractors), id, - monitorProperties.getMaxDashes()); + monitorProperties.getMaxDashes(), monitorProperties.getMaxPaths(), + monitorProperties.getIgnoredPaths()); } } diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/MonitorConfigurationProperties.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/MonitorConfigurationProperties.java index 296bd9f5a..635d7d080 100644 --- a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/MonitorConfigurationProperties.java +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/MonitorConfigurationProperties.java @@ -16,6 +16,9 @@ package org.springframework.cloud.config.monitor; +import java.util.ArrayList; +import java.util.List; + import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("spring.cloud.config.server.monitor") @@ -27,6 +30,11 @@ public class MonitorConfigurationProperties { */ public static final int DEFAULT_MAX_DASHES = 20; + /** + * Default maximum number of paths processed from a notification. + */ + public static final int DEFAULT_MAX_PATHS = 20; + private Endpoint endpoint = new Endpoint(); /** @@ -36,6 +44,10 @@ public class MonitorConfigurationProperties { */ private int maxDashes = DEFAULT_MAX_DASHES; + private int maxPaths = DEFAULT_MAX_PATHS; + + private List ignoredPaths = new ArrayList<>(); + private GitHub github = new GitHub(); private Gogs gogs = new Gogs(); @@ -112,6 +124,22 @@ public void setMaxDashes(int maxDashes) { this.maxDashes = maxDashes; } + public int getMaxPaths() { + return maxPaths; + } + + public void setMaxPaths(int maxPaths) { + this.maxPaths = maxPaths; + } + + public List getIgnoredPaths() { + return ignoredPaths; + } + + public void setIgnoredPaths(List ignoredPaths) { + this.ignoredPaths = ignoredPaths; + } + public static class Endpoint { private String path = "/monitor"; diff --git a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java index 2c35735c4..fe5c58742 100644 --- a/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java +++ b/spring-cloud-config-monitor/src/main/java/org/springframework/cloud/config/monitor/PropertyPathEndpoint.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -66,14 +67,32 @@ public class PropertyPathEndpoint implements ApplicationEventPublisherAware { */ private final int maxDashes; + /** + * Upper bound on the number of paths processed from a notification. Prevents an + * externally supplied notification from causing an unbounded amount of path + * processing. Configurable via {@code spring.cloud.config.server.monitor.max-paths}. + */ + private final int maxPaths; + + private final List ignoredPaths; + public PropertyPathEndpoint(PropertyPathNotificationExtractor extractor, String busId) { - this(extractor, busId, MonitorConfigurationProperties.DEFAULT_MAX_DASHES); + this(extractor, busId, MonitorConfigurationProperties.DEFAULT_MAX_DASHES, + MonitorConfigurationProperties.DEFAULT_MAX_PATHS, Collections.emptyList()); } public PropertyPathEndpoint(PropertyPathNotificationExtractor extractor, String busId, int maxDashes) { + this(extractor, busId, maxDashes, MonitorConfigurationProperties.DEFAULT_MAX_PATHS, Collections.emptyList()); + } + + public PropertyPathEndpoint(PropertyPathNotificationExtractor extractor, String busId, int maxDashes, int maxPaths, + List ignoredPaths) { this.extractor = extractor; this.busId = busId; this.maxDashes = maxDashes; + this.maxPaths = maxPaths; + this.ignoredPaths = ignoredPaths == null ? Collections.emptyList() + : ignoredPaths.stream().map(Pattern::compile).toList(); } /* for testing */ String getBusId() { @@ -92,8 +111,19 @@ public Set notifyByPath(@RequestHeader HttpHeaders headers, @RequestBody Set services = new LinkedHashSet<>(); - for (String path : notification.getPaths()) { - services.addAll(guessServiceName(path)); + String[] paths = notification.getPaths(); + if (paths != null) { + int pathCount = Math.min(paths.length, this.maxPaths); + for (int i = 0; i < pathCount; i++) { + String path = paths[i]; + if (!isIgnored(path)) { + services.addAll(guessServiceName(path)); + } + } + if (paths.length > this.maxPaths) { + log.warn("Number of paths in notification exceeds the configured maximum of " + this.maxPaths + + " (spring.cloud.config.server.monitor.max-paths); stopping path processing early"); + } } if (this.applicationEventPublisher != null) { for (String service : services) { @@ -144,4 +174,8 @@ private Set guessServiceName(String path) { return services; } + private boolean isIgnored(String path) { + return path != null && this.ignoredPaths.stream().anyMatch(pattern -> pattern.matcher(path).matches()); + } + } diff --git a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfigurationTests.java b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfigurationTests.java index cb01701c7..fc03262d7 100644 --- a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfigurationTests.java +++ b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/EnvironmentMonitorAutoConfigurationTests.java @@ -67,6 +67,23 @@ public void testCanAddCustomPropertyPathNotificationExtractor() { context.close(); } + @Test + public void testMonitorPathProperties() { + ConfigurableApplicationContext context = new SpringApplicationBuilder(BusConfig.class, + EnvironmentMonitorAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class, + ServerProperties.class, PropertyPlaceholderAutoConfiguration.class) + .properties("server.port=-1", "spring.cloud.config.server.monitor.max-paths=1", + "spring.cloud.config.server.monitor.ignored-paths=docker-compose/.*") + .run(); + + PropertyPathEndpoint endpoint = context.getBean(PropertyPathEndpoint.class); + + assertThat(ReflectionTestUtils.getField(endpoint, "maxPaths")).isEqualTo(1); + assertThat((Collection) ReflectionTestUtils.getField(endpoint, "ignoredPaths")).hasSize(1); + + context.close(); + } + @Configuration(proxyBeanMethods = false) static class BusConfig { diff --git a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java index 8b4a14a84..0175f5a1a 100644 --- a/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java +++ b/spring-cloud-config-monitor/src/test/java/org/springframework/cloud/config/monitor/PropertyPathEndpointTests.java @@ -119,4 +119,37 @@ public void testNotifyLimitsDashes() { .hasSize(2); } + @Test + public void testNotifyIgnoresConfiguredPaths() { + PropertyPathEndpoint ignoredEndpoint = new PropertyPathEndpoint( + new CompositePropertyPathNotificationExtractor(Collections.emptyList()), "abc1", 20, 20, + Collections.singletonList("docker-compose/.*")); + StaticApplicationContext publisher = new StaticApplicationContext(); + ignoredEndpoint.setApplicationEventPublisher(publisher); + publisher.refresh(); + + List request = new ArrayList<>(); + request.add("docker-compose/docker-compose.yml"); + request.add("foo.yml"); + + assertThat(ignoredEndpoint.notifyByForm(new HttpHeaders(), request).toString()).isEqualTo("[foo]"); + } + + @Test + public void testNotifyLimitsPaths() { + PropertyPathEndpoint limitedEndpoint = new PropertyPathEndpoint( + new CompositePropertyPathNotificationExtractor(Collections.emptyList()), "abc1", 20, 2, + Collections.emptyList()); + StaticApplicationContext publisher = new StaticApplicationContext(); + limitedEndpoint.setApplicationEventPublisher(publisher); + publisher.refresh(); + + List request = new ArrayList<>(); + request.add("foo.yml"); + request.add("bar.yml"); + request.add("baz.yml"); + + assertThat(limitedEndpoint.notifyByForm(new HttpHeaders(), request).toString()).isEqualTo("[foo, bar]"); + } + }