Remote Events migrated from Spring Bus to Spring Cloud Stream (#2563)

* Remote Events migrated from Spring Bus to Spring Cloud Stream

---------

Co-authored-by: vasilchev <vasil.ilchev@bosch.com>
This commit is contained in:
Vasil Ilchev
2025-07-30 16:58:00 +03:00
committed by GitHub
parent 10da0288d9
commit 4a8e60764f
49 changed files with 1147 additions and 461 deletions

View File

@@ -35,7 +35,7 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
@@ -9,63 +9,174 @@
*/
package org.eclipse.hawkbit.repository.event;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.Set;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.event.remote.AbstractRemoteEvent;
import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent;
import org.eclipse.hawkbit.repository.event.remote.MultiActionCancelEvent;
import org.eclipse.hawkbit.repository.event.remote.MultiActionAssignEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.service.CancelTargetAssignmentServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.MultiActionAssignServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.MultiActionCancelServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAssignDistributionSetServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAttributesRequestedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetCreatedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetDeletedServiceEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bus.BusProperties;
import org.springframework.cloud.bus.ServiceMatcher;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
/**
* A singleton bean which holds the event publisher and service origin id in order to publish remote application events.
* It can be used in beans not instantiated by spring e.g. JPA entities which cannot be auto-wired.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("java:S6548") // java:S6548 - singleton holder ensures static access to spring resources in some places
@Slf4j
public final class EventPublisherHolder {
@Value("${org.eclipse.hawkbit.events.remote-enabled:true}")
private boolean remoteEventsEnabled;
@Value("${org.eclipse.hawkbit.events.remote.destination:fanoutEventChannel}")
private String fanoutEventChannel;
@Value("${org.eclipse.hawkbit.events.remote-service-enabled:true}")
private boolean remoteServiceEventsEnabled;
@Value("${org.eclipse.hawkbit.events.remote.service.destination:serviceEventChannel}")
private String serviceEventChannel;
private static final EventPublisherHolder SINGLETON = new EventPublisherHolder();
private ApplicationEventPublisher delegateEventPublisher;
private StreamBridge streamBridge;
@Getter
private ApplicationEventPublisher eventPublisher;
private ServiceMatcher serviceMatcher;
private BusProperties bus;
/**
* @return the event publisher holder singleton instance
*/
public static EventPublisherHolder getInstance() {
return SINGLETON;
}
@Autowired // spring setter injection
public void setApplicationEventPublisher(final ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Autowired(required = false) // spring setter injection
public void setServiceMatcher(final ServiceMatcher serviceMatcher) {
this.serviceMatcher = serviceMatcher;
}
@Autowired(required = false) // spring setter injection
public void setBusProperties(final BusProperties bus) {
this.bus = bus;
}
/**
* @return the service origin Id coming either from {@link ServiceMatcher} when available or {@link BusProperties} otherwise.
*/
public String getApplicationId() {
String id = null;
if (serviceMatcher != null) {
id = serviceMatcher.getBusId();
@PostConstruct
private void validateRemoteEventConfig() {
if (remoteEventsEnabled && streamBridge == null) {
throw new IllegalStateException("'org.eclipse.hawkbit.events.remote-enabled' is true but streamBridge is not configured. Check if 'spring-cloud-starter-stream-rabbit' dependency is included.");
}
if (id == null && bus != null) {
id = bus.getId();
}
public static final Set<Class<?>> SERVICE_EVENTS = Set.of(
TargetCreatedEvent.class,
TargetDeletedEvent.class,
MultiActionAssignEvent.class,
MultiActionCancelEvent.class,
TargetAssignDistributionSetEvent.class,
TargetAttributesRequestedEvent.class,
CancelTargetAssignmentEvent.class
);
@Autowired
public void setApplicationEventPublisher(final ApplicationEventPublisher delegate) {
this.delegateEventPublisher = delegate;
}
@Autowired(required = false)
public void setStreamBridge(final StreamBridge streamBridge) {
this.streamBridge = streamBridge;
}
public ApplicationEventPublisher getEventPublisher() {
return new RoutingEventPublisher(streamBridge, delegateEventPublisher);
}
class RoutingEventPublisher implements ApplicationEventPublisher {
private final StreamBridge streamBridge;
private final ApplicationEventPublisher delegate;
public RoutingEventPublisher(final StreamBridge streamBridge, final ApplicationEventPublisher delegate) {
this.streamBridge = streamBridge;
this.delegate = delegate;
}
@Override
public void publishEvent(final Object event) {
routeEvent(event);
}
@Override
public void publishEvent(final ApplicationEvent event) {
routeEvent(event);
}
private void routeEvent(Object event) {
if (remoteEventsEnabled && event instanceof AbstractRemoteEvent remoteEvent) {
// send events to remote nodes
publishRemotely(remoteEvent);
} else {
// publish locally
publishLocally(event);
}
}
private void publishRemotely(final AbstractRemoteEvent remoteEvent) {
streamBridge.send(fanoutEventChannel, remoteEvent);
// some events need to be processed only by single service replica
// wrap the entity event into a service event and send it to the service channel
if (shouldForwardAsServiceEvent(remoteEvent)) {
final AbstractRemoteEvent serviceEvent = toServiceEvent(remoteEvent);
if (serviceEvent != null) {
log.debug("Publishing Service event: {} to remote channel: {}", serviceEvent, serviceEventChannel);
streamBridge.send(serviceEventChannel, serviceEvent);
} else {
log.error("No Service event created for: {}. Skipping send Service event to Service channel. {}", remoteEvent.getClass(),
serviceEventChannel);
}
}
}
private void publishLocally(final Object event) {
delegate.publishEvent(event);
// check if the event should be forwarded as a service event even if it is not a remote event
if (shouldForwardAsServiceEvent(event)) {
final AbstractRemoteEvent serviceEvent = toServiceEvent((AbstractRemoteEvent) event);
if (serviceEvent != null) {
log.debug("Publishing Service event: {} to locally.", serviceEvent);
delegate.publishEvent(serviceEvent);
} else {
log.error("No Service event created for: {}. Skipping send Service event locally.", event.getClass());
}
}
}
/**
* Checks if the event should be forwarded as a service event.
* If remote service events are enabled and the event is one of the service events,
*
* @param remoteEvent the event to check whether it should be forwarded as a service event
* @return true if the event should be forwarded as a service event, false otherwise
*/
private boolean shouldForwardAsServiceEvent(final Object remoteEvent) {
return remoteServiceEventsEnabled && SERVICE_EVENTS.contains(remoteEvent.getClass());
}
private AbstractRemoteEvent toServiceEvent(final AbstractRemoteEvent event) {
if (event instanceof TargetAssignDistributionSetEvent targetAssignDistributionSetEvent) {
return new TargetAssignDistributionSetServiceEvent(targetAssignDistributionSetEvent);
} else if (event instanceof MultiActionAssignEvent multiActionAssignEvent) {
return new MultiActionAssignServiceEvent(multiActionAssignEvent);
} else if (event instanceof MultiActionCancelEvent multiActionCancelEvent) {
return new MultiActionCancelServiceEvent(multiActionCancelEvent);
} else if (event instanceof CancelTargetAssignmentEvent cancelTargetAssignmentEvent) {
return new CancelTargetAssignmentServiceEvent(cancelTargetAssignmentEvent);
} else if (event instanceof TargetDeletedEvent targetDeletedEvent) {
return new TargetDeletedServiceEvent(targetDeletedEvent);
} else if (event instanceof TargetCreatedEvent targetCreatedEvent) {
return new TargetCreatedServiceEvent(targetCreatedEvent);
} else if (event instanceof TargetAttributesRequestedEvent targetAttributesRequestedEvent) {
return new TargetAttributesRequestedServiceEvent(targetAttributesRequestedEvent);
}
return null;
}
// due to a bug (?) in Spring Cloud, we cannot pass null for applicationId
return id == null ? "" : id;
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@Getter
@EqualsAndHashCode(callSuper = false)
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class AbstractRemoteEvent extends ApplicationEvent {
private final String id;
protected AbstractRemoteEvent() {
this("_empty_default_");
}
protected AbstractRemoteEvent(Object source) {
super(source);
this.id = UUID.randomUUID().toString();
}
}

View File

@@ -10,15 +10,14 @@
package org.eclipse.hawkbit.repository.event.remote;
import java.io.Serial;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.eclipse.hawkbit.repository.event.EventPublisherHolder;
import org.eclipse.hawkbit.repository.event.TenantAwareEvent;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
/**
* A distributed tenant aware event. It's the base class of the other
@@ -29,19 +28,21 @@ import org.springframework.cloud.bus.event.RemoteApplicationEvent;
@Getter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class RemoteTenantAwareEvent extends RemoteApplicationEvent implements TenantAwareEvent {
public class RemoteTenantAwareEvent extends AbstractRemoteEvent implements TenantAwareEvent {
@Serial
private static final long serialVersionUID = 1L;
private String tenant;
/**
* Constructor.
*
* @param source the for the remote event.
* @param tenant the tenant
*/
public RemoteTenantAwareEvent(final String tenant, final Object source) {
super(source == null ? getApplicationId() : source, getApplicationId(), DEFAULT_DESTINATION_FACTORY.getDestination(null));
super(source == null ? UUID.randomUUID() : source);
this.tenant = tenant;
}
private static String getApplicationId() {
return EventPublisherHolder.getInstance().getApplicationId();
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote.service;
import lombok.Getter;
import org.eclipse.hawkbit.repository.event.remote.AbstractRemoteEvent;
@Getter
public abstract class AbstractServiceRemoteEvent<T extends AbstractRemoteEvent> extends AbstractRemoteEvent {
private final T remoteEvent;
protected AbstractServiceRemoteEvent(T remoteEvent) {
super(remoteEvent == null ? "_empty_source_" : remoteEvent.getSource());
this.remoteEvent = remoteEvent;
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote.service;
import java.io.Serial;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent;
/**
* Service event for {@link CancelTargetAssignmentEvent}. Event that needs single replica processing
*/
public class CancelTargetAssignmentServiceEvent extends AbstractServiceRemoteEvent<CancelTargetAssignmentEvent> {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator
public CancelTargetAssignmentServiceEvent(@JsonProperty("payload") final CancelTargetAssignmentEvent remoteEvent) {
super(remoteEvent);
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote.service;
import java.io.Serial;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.eclipse.hawkbit.repository.event.remote.MultiActionAssignEvent;
/**
* Service event for {@link MultiActionAssignEvent}. Event that needs single replica processing
*/
public class MultiActionAssignServiceEvent extends AbstractServiceRemoteEvent<MultiActionAssignEvent> {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator
public MultiActionAssignServiceEvent(@JsonProperty("payload") final MultiActionAssignEvent remoteEvent) {
super(remoteEvent);
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote.service;
import java.io.Serial;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.eclipse.hawkbit.repository.event.remote.MultiActionCancelEvent;
/**
* Service event for {@link MultiActionCancelEvent}. Event that needs single replica processing
*/
public class MultiActionCancelServiceEvent extends AbstractServiceRemoteEvent<MultiActionCancelEvent> {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator
public MultiActionCancelServiceEvent(@JsonProperty("payload") final MultiActionCancelEvent remoteEvent) {
super(remoteEvent);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote.service;
import java.io.Serial;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
/**
* Service event for {@link TargetAssignDistributionSetEvent}. Event that needs single replica processing
*/
public class TargetAssignDistributionSetServiceEvent extends AbstractServiceRemoteEvent<TargetAssignDistributionSetEvent> {
@Serial
private static final long serialVersionUID = 1L;
/**
* Constructor.
*
* @param remoteEvent the remote event to group
*/
@JsonCreator
public TargetAssignDistributionSetServiceEvent(@JsonProperty("payload") final TargetAssignDistributionSetEvent remoteEvent) {
super(remoteEvent);
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote.service;
import java.io.Serial;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
/**
* Service event for {@link TargetAttributesRequestedEvent}. Event that needs single replica processing
*/
public class TargetAttributesRequestedServiceEvent extends AbstractServiceRemoteEvent<TargetAttributesRequestedEvent> {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator
public TargetAttributesRequestedServiceEvent(@JsonProperty("payload") final TargetAttributesRequestedEvent remoteEvent) {
super(remoteEvent);
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote.service;
import java.io.Serial;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
/**
* Service event for {@link TargetCreatedEvent}. Event that needs single replica processing
*/
public class TargetCreatedServiceEvent extends AbstractServiceRemoteEvent<TargetCreatedEvent> {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator
public TargetCreatedServiceEvent(@JsonProperty("payload") final TargetCreatedEvent remoteEvent) {
super(remoteEvent);
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote.service;
import java.io.Serial;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
/**
* Service event for {@link TargetDeletedEvent}. Event that needs single replica processing
*/
public class TargetDeletedServiceEvent extends AbstractServiceRemoteEvent<TargetDeletedEvent> {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator
public TargetDeletedServiceEvent(@JsonProperty("payload") final TargetDeletedEvent remoteEvent) {
super(remoteEvent);
}
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.event;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.util.MimeType;
public class EventJacksonMessageConverter extends MappingJackson2MessageConverter {
public EventJacksonMessageConverter() {
super(new MimeType("application", "remote-event-json"));
ObjectMapper objectMapper = new ObjectMapper();
EventType.getNamedTypes().forEach(objectMapper::registerSubtypes);
setObjectMapper(objectMapper);
}
}

View File

@@ -14,7 +14,7 @@ import io.protostuff.ProtobufIOUtil;
import io.protostuff.Schema;
import io.protostuff.runtime.RuntimeSchema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.eclipse.hawkbit.repository.event.remote.AbstractRemoteEvent;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
@@ -30,20 +30,20 @@ import org.springframework.util.MimeType;
* values of {@link EventType}.
*/
@Slf4j
public class BusProtoStuffMessageConverter extends AbstractMessageConverter {
public class EventProtoStuffMessageConverter extends AbstractMessageConverter {
public static final MimeType APPLICATION_BINARY_PROTOSTUFF = new MimeType("application", "binary+protostuff");
/** The length of the class type length of the payload. */
private static final byte EVENT_TYPE_LENGTH = 2;
public BusProtoStuffMessageConverter() {
public EventProtoStuffMessageConverter() {
super(APPLICATION_BINARY_PROTOSTUFF);
}
@Override
protected boolean supports(final Class<?> aClass) {
return RemoteApplicationEvent.class.isAssignableFrom(aClass);
return AbstractRemoteEvent.class.isAssignableFrom(aClass);
}
@Override

View File

@@ -10,23 +10,18 @@
package org.eclipse.hawkbit.event;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import io.protostuff.ProtostuffIOUtil;
import io.protostuff.Schema;
import org.eclipse.hawkbit.repository.event.ApplicationEventFilter;
import org.eclipse.hawkbit.repository.event.EventPublisherHolder;
import org.eclipse.hawkbit.repository.event.remote.AbstractRemoteEvent;
import org.eclipse.hawkbit.repository.event.remote.RemoteTenantAwareEvent;
import org.eclipse.hawkbit.repository.event.EventPublisherHolder;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.bus.BusProperties;
import org.springframework.cloud.bus.ConditionalOnBusEnabled;
import org.springframework.cloud.bus.ServiceMatcher;
import org.springframework.cloud.bus.jackson.RemoteApplicationEventScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@@ -37,12 +32,10 @@ import org.springframework.core.ResolvableType;
import org.springframework.messaging.converter.MessageConverter;
/**
* Autoconfiguration for the event bus.
* Autoconfiguration for the events.
*/
@Configuration
@RemoteApplicationEventScan(basePackages = "org.eclipse.hawkbit.repository.event.remote")
@PropertySource("classpath:/hawkbit-eventbus-defaults.properties")
@EnableConfigurationProperties(BusProperties.class)
@PropertySource("classpath:/hawkbit-events-defaults.properties")
public class EventPublisherConfiguration {
/**
@@ -66,7 +59,7 @@ public class EventPublisherConfiguration {
* @return the singleton instance of the {@link EventPublisherHolder}
*/
@Bean
EventPublisherHolder eventBusHolder() {
public EventPublisherHolder eventPublisherHolder() {
return EventPublisherHolder.getInstance();
}
@@ -84,19 +77,12 @@ public class EventPublisherConfiguration {
private final SystemSecurityContext systemSecurityContext;
private final ApplicationEventFilter applicationEventFilter;
private ServiceMatcher serviceMatcher;
protected TenantAwareApplicationEventPublisher(
final SystemSecurityContext systemSecurityContext, final ApplicationEventFilter applicationEventFilter) {
this.systemSecurityContext = systemSecurityContext;
this.applicationEventFilter = applicationEventFilter;
}
@Autowired(required = false)
public void setServiceMatcher(final ServiceMatcher serviceMatcher) {
this.serviceMatcher = serviceMatcher;
}
/**
* Was overridden that not every event has to run within an own tenantAware.
*/
@@ -106,33 +92,53 @@ public class EventPublisherConfiguration {
return;
}
if (serviceMatcher == null || !(event instanceof final RemoteTenantAwareEvent remoteEvent)) {
super.multicastEvent(event, eventType);
if (event instanceof final RemoteTenantAwareEvent remoteEvent) {
systemSecurityContext.runAsSystemAsTenant(() -> {
super.multicastEvent(event, eventType);
return null;
}, remoteEvent.getTenant());
return;
}
if (serviceMatcher.isFromSelf(remoteEvent)) {
super.multicastEvent(event, eventType);
return;
}
systemSecurityContext.runAsSystemAsTenant(() -> {
super.multicastEvent(event, eventType);
return null;
}, remoteEvent.getTenant());
super.multicastEvent(event, eventType);
}
}
@ConditionalOnBusEnabled
@ConditionalOnClass({ Schema.class, ProtostuffIOUtil.class })
protected static class BusProtoStuffAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "org.eclipse.hawkbit.events.remote-enabled", havingValue = "true")
public Consumer<AbstractRemoteEvent> serviceEventConsumer(ApplicationEventPublisher publisher) {
return publisher::publishEvent;
}
@Bean
@ConditionalOnProperty(name = "org.eclipse.hawkbit.events.remote-enabled", havingValue = "true")
public Consumer<AbstractRemoteEvent> fanoutEventConsumer(ApplicationEventPublisher publisher) {
return publisher::publishEvent;
}
@ConditionalOnProperty(name = "org.eclipse.hawkbit.events.remote-enabled", havingValue = "true")
@ConditionalOnProperty(name = "spring.cloud.stream.default.content-type", havingValue = "application/binary+stuff")
protected static class EventProtoStuffAutoConfiguration {
/**
* @return the protostuff io message converter
* @return the protostuff io message converter for events
*/
@Bean
public MessageConverter busProtoBufConverter() {
return new BusProtoStuffMessageConverter();
public MessageConverter eventProtoStuffConverter() {
return new EventProtoStuffMessageConverter();
}
}
@ConditionalOnProperty(name = "org.eclipse.hawkbit.events.remote-enabled", havingValue = "true")
@ConditionalOnProperty(name = "spring.cloud.stream.default.content-type", havingValue = "application/remote-event-json")
protected static class EventJacksonAutoConfiguration {
/**
* @return the Jackson message converter for events
*/
@Bean
public MessageConverter eventJacksonMessageConverter() {
return new EventJacksonMessageConverter();
}
}
}

View File

@@ -9,10 +9,12 @@
*/
package org.eclipse.hawkbit.event;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -24,6 +26,13 @@ import org.eclipse.hawkbit.repository.event.remote.DistributionSetTypeDeletedEve
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
import org.eclipse.hawkbit.repository.event.remote.MultiActionAssignEvent;
import org.eclipse.hawkbit.repository.event.remote.MultiActionCancelEvent;
import org.eclipse.hawkbit.repository.event.remote.service.CancelTargetAssignmentServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.MultiActionAssignServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.MultiActionCancelServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAssignDistributionSetServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAttributesRequestedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetCreatedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetDeletedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.RolloutDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.RolloutGroupDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.RolloutStoppedEvent;
@@ -166,6 +175,15 @@ public class EventType {
TYPES.put(44, TargetTypeCreatedEvent.class);
TYPES.put(45, TargetTypeUpdatedEvent.class);
TYPES.put(46, TargetTypeDeletedEvent.class);
// processing events - start from 1000 to leave room for future db events
TYPES.put(1000, TargetCreatedServiceEvent.class);
TYPES.put(1001, TargetDeletedServiceEvent.class);
TYPES.put(1002, TargetAssignDistributionSetServiceEvent.class);
TYPES.put(1003, TargetAttributesRequestedServiceEvent.class);
TYPES.put(1004, CancelTargetAssignmentServiceEvent.class);
TYPES.put(1005, MultiActionAssignServiceEvent.class);
TYPES.put(1006, MultiActionCancelServiceEvent.class);
}
/**
@@ -197,4 +215,10 @@ public class EventType {
public Class<?> getTargetClass() {
return TYPES.get(value);
}
public static Collection<NamedType> getNamedTypes() {
return TYPES.entrySet().stream()
.map(e -> new NamedType(e.getValue(), String.valueOf(e.getKey())))
.toList();
}
}

View File

@@ -1,21 +0,0 @@
#
# Copyright (c) 2015 Bosch Software Innovations GmbH and others
#
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Spring cloud bus and stream
spring.cloud.bus.enabled=true
# Disable Cloud Bus default events
spring.cloud.bus.env.enabled=false
spring.cloud.bus.ack.enabled=false
spring.cloud.bus.trace.enabled=false
spring.cloud.bus.refresh.enabled=false
# Disable Cloud Bus endpoints
management.endpoint.busrefresh.access=none
management.endpoint.busenv.access=none
# Spring cloud bus and stream END

View File

@@ -0,0 +1,47 @@
#
# Copyright (c) 2015 Bosch Software Innovations GmbH and others
#
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
org.eclipse.hawkbit.events.remote-enabled=true
spring.cloud.function.definition=fanoutEventConsumer;serviceEventConsumer
spring.cloud.stream.default.content-type=application/remote-event-json
# -- Consumer bindings --
spring.cloud.stream.bindings.fanoutEventConsumer-in-0.destination=fanoutEventChannel
spring.cloud.stream.bindings.serviceEventConsumer-in-0.destination=serviceEventChannel
# -- Producer bindings (for StreamBridge) --
spring.cloud.stream.bindings.fanoutEventChannel.destination=fanoutEventChannel
spring.cloud.stream.bindings.serviceEventChannel.destination=serviceEventChannel
spring.cloud.stream.bindings.serviceEventConsumer-in-0.group=${spring.application.name}
# Performance
spring.cloud.stream.rabbit.binder.compressionLevel=0
spring.cloud.stream.rabbit.bindings.fanoutEventConsumer-in-0.consumer.anonymousGroupPrefix=${spring.application.name}-
spring.cloud.stream.rabbit.bindings.fanoutEventConsumer-in-0.consumer.durableSubscription=false
spring.cloud.stream.rabbit.bindings.fanoutEventConsumer-in-0.consumer.maxConcurrency=1
spring.cloud.stream.rabbit.bindings.fanoutEventConsumer-in-0.consumer.requeueRejected=false
spring.cloud.stream.rabbit.bindings.fanoutEventConsumer-in-0.consumer.prefetch=100
spring.cloud.stream.rabbit.bindings.serviceEventConsumer-in-0.consumer.maxConcurrency=1
spring.cloud.stream.rabbit.bindings.serviceEventConsumer-in-0.consumer.requeueRejected=false
spring.cloud.stream.rabbit.bindings.serviceEventConsumer-in-0.consumer.prefetch=100
spring.cloud.stream.rabbit.bindings.fanoutEventChannel.producer.declareExchange=false
spring.cloud.stream.rabbit.bindings.fanoutEventChannel.producer.batchingEnabled=true
spring.cloud.stream.rabbit.bindings.fanoutEventChannel.producer.batchSize=1000
spring.cloud.stream.rabbit.bindings.fanoutEventChannel.producer.batch-buffer-limit=100000
spring.cloud.stream.rabbit.bindings.fanoutEventChannel.producer.deliveryMode=NON_PERSISTENT
spring.cloud.stream.rabbit.bindings.serviceEventChannel.producer.declareExchange=false
spring.cloud.stream.rabbit.bindings.serviceEventChannel.producer.batchingEnabled=true
spring.cloud.stream.rabbit.bindings.serviceEventChannel.producer.batchSize=1000
spring.cloud.stream.rabbit.bindings.serviceEventChannel.producer.batch-buffer-limit=100000
spring.cloud.stream.rabbit.bindings.serviceEventChannel.producer.deliveryMode=NON_PERSISTENT

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.event;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import org.eclipse.hawkbit.repository.event.remote.AbstractRemoteEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.model.Target;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
@ExtendWith(MockitoExtension.class)
class EventJacksonMessageConverterTest {
private final TestEventJacksonMessageConverter underTest = new TestEventJacksonMessageConverter();
@Mock
private Target targetMock;
@Mock
private Message<Object> messageMock;
@BeforeEach
void before() {
when(targetMock.getId()).thenReturn(1L);
}
/**
* Verifies that the TargetCreatedEvent can be successfully serialized and deserialized
*/
@Test
void successfullySerializeAndDeserializeEvent() {
final TargetCreatedEvent targetCreatedEvent = new TargetCreatedEvent(targetMock);
// serialize
final Object serializedEvent = underTest.convertToInternal(targetCreatedEvent,
new MessageHeaders(new HashMap<>()), null);
assertThat(serializedEvent).isInstanceOf(byte[].class);
// deserialize
when(messageMock.getPayload()).thenReturn(serializedEvent);
final Object deserializedEvent = underTest.convertFromInternal(messageMock, AbstractRemoteEvent.class, null);
assertThat(deserializedEvent)
.isInstanceOf(TargetCreatedEvent.class)
.isEqualTo(targetCreatedEvent);
}
/**
* Test subclass to expose protected methods for testing.
*/
private static class TestEventJacksonMessageConverter extends EventJacksonMessageConverter {
@Override
public Object convertToInternal(Object payload, MessageHeaders headers, Object conversionHint) {
return super.convertToInternal(payload, headers, conversionHint);
}
@Override
public Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) {
return super.convertFromInternal(message, targetClass, conversionHint);
}
}
}

View File

@@ -16,6 +16,7 @@ import static org.mockito.Mockito.when;
import java.io.Serial;
import java.util.HashMap;
import org.eclipse.hawkbit.repository.event.remote.AbstractRemoteEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.RemoteEntityEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.model.Target;
@@ -24,15 +25,14 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.MessageConversionException;
@ExtendWith(MockitoExtension.class)
class BusProtoStuffMessageConverterTest {
class EventProtoStuffMessageConverterTest {
private final BusProtoStuffMessageConverter underTest = new BusProtoStuffMessageConverter();
private final EventProtoStuffMessageConverter underTest = new EventProtoStuffMessageConverter();
@Mock
private Target targetMock;
@@ -58,7 +58,7 @@ class BusProtoStuffMessageConverterTest {
// deserialize
when(messageMock.getPayload()).thenReturn(serializedEvent);
final Object deserializedEvent = underTest.convertFromInternal(messageMock, RemoteApplicationEvent.class, null);
final Object deserializedEvent = underTest.convertFromInternal(messageMock, AbstractRemoteEvent.class, null);
assertThat(deserializedEvent)
.isInstanceOf(TargetCreatedEvent.class)
.isEqualTo(targetCreatedEvent);

View File

@@ -30,7 +30,6 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.event.remote.RemoteTenantAwareEvent;
import org.eclipse.hawkbit.repository.event.remote.TenantConfigurationDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.RemoteEntityEvent;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
@@ -58,7 +57,6 @@ import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.bus.ServiceMatcher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.core.convert.support.ConfigurableConversionService;
@@ -89,9 +87,7 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
private final CacheManager cacheManager;
private final AfterTransactionCommitExecutor afterCommitExecutor;
private ServiceMatcher serviceMatcher;
protected JpaTenantConfigurationManagement(
public JpaTenantConfigurationManagement(
final TenantConfigurationRepository tenantConfigurationRepository,
final TenantConfigurationProperties tenantConfigurationProperties,
final CacheManager cacheManager, final AfterTransactionCommitExecutor afterCommitExecutor,
@@ -103,11 +99,6 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
this.applicationContext = applicationContext;
}
@Autowired(required = false)
public void setServiceMatcher(final ServiceMatcher serviceMatcher) {
this.serviceMatcher = serviceMatcher;
}
@Override
@CacheEvict(value = "tenantConfiguration", key = "#configurationKeyName")
@Transactional
@@ -186,10 +177,6 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
*/
@EventListener
public void onTenantConfigurationDeletedEvent(final TenantConfigurationDeletedEvent event) {
if (!shouldProcessRemoteTenantAwareEvent(event)) {
return;
}
evictCacheEntryByKeyIfPresent(event.getConfigKey());
}
@@ -200,10 +187,6 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
*/
@EventListener
public void onTenantConfigurationRemoteEntityEvent(final RemoteEntityEvent<TenantConfiguration> event) {
if (!shouldProcessRemoteTenantAwareEvent(event)) {
return;
}
event.getEntity().ifPresent(tenantConfiguration -> evictCacheEntryByKeyIfPresent(tenantConfiguration.getKey()));
}
@@ -391,8 +374,4 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
cache.evictIfPresent(key);
}
}
private boolean shouldProcessRemoteTenantAwareEvent(final RemoteTenantAwareEvent event) {
return serviceMatcher == null || !serviceMatcher.isFromSelf(event) && serviceMatcher.isForSelf(event);
}
}

View File

@@ -51,7 +51,6 @@ import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
import org.eclipse.hawkbit.repository.jpa.utils.MapAttributeConverter;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.ActionStatus;
import org.eclipse.hawkbit.repository.model.BaseEntity;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;

View File

@@ -11,49 +11,42 @@ package org.eclipse.hawkbit.repository.event.remote;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.hawkbit.event.BusProtoStuffMessageConverter;
import org.eclipse.hawkbit.event.EventProtoStuffMessageConverter;
import org.eclipse.hawkbit.repository.event.TenantAwareEvent;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.cloud.bus.jackson.BusJacksonAutoConfiguration;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.MutableMessageHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.MimeTypeUtils;
/**
* Test the remote entity events.
*/
@TestPropertySource(properties = { "spring.cloud.bus.enabled=true" })
@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes
public abstract class AbstractRemoteEventTest extends AbstractJpaIntegrationTest {
@Import(AbstractRemoteEventTest.EventProtoStuffTestConfig.class)
public abstract class AbstractRemoteEventTest extends AbstractJpaIntegrationTest {
@Autowired
private BusProtoStuffMessageConverter busProtoStuffMessageConverter;
private EventProtoStuffMessageConverter eventProtoStuffMessageConverter;
private AbstractMessageConverter jacksonMessageConverter;
@BeforeEach
public void setup() throws Exception {
final BusJacksonAutoConfiguration autoConfiguration = new BusJacksonAutoConfiguration();
this.jacksonMessageConverter = autoConfiguration.busJsonConverter(null);
ReflectionTestUtils.setField(
jacksonMessageConverter, "packagesToScan",
new String[] { "org.eclipse.hawkbit.repository.event.remote", ClassUtils.getPackageName(RemoteApplicationEvent.class) });
((InitializingBean) jacksonMessageConverter).afterPropertiesSet();
public void setup() {
this.jacksonMessageConverter = new MappingJackson2MessageConverter();
}
@SuppressWarnings("unchecked")
@@ -65,24 +58,33 @@ import org.springframework.util.MimeTypeUtils;
@SuppressWarnings("unchecked")
protected <T extends TenantAwareEvent> T createProtoStuffEvent(final T event) {
final Message<?> message = createProtoStuffMessage(event);
return (T) busProtoStuffMessageConverter.fromMessage(message, event.getClass());
return (T) eventProtoStuffMessageConverter.fromMessage(message, event.getClass());
}
private Message<?> createProtoStuffMessage(final TenantAwareEvent event) {
final Map<String, Object> headers = new LinkedHashMap<>();
headers.put(MessageHeaders.CONTENT_TYPE, BusProtoStuffMessageConverter.APPLICATION_BINARY_PROTOSTUFF);
return busProtoStuffMessageConverter.toMessage(event, new MutableMessageHeaders(headers));
return eventProtoStuffMessageConverter.toMessage(
event, new MutableMessageHeaders(Map.of(MessageHeaders.CONTENT_TYPE,
EventProtoStuffMessageConverter.APPLICATION_BINARY_PROTOSTUFF))
);
}
private Message<String> createJsonMessage(final Object event) {
final Map<String, MimeType> headers = new LinkedHashMap<>();
headers.put(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON);
try {
final String json = new ObjectMapper().writeValueAsString(event);
return MessageBuilder.withPayload(json).copyHeaders(headers).build();
} catch (final JsonProcessingException e) {
String json = new ObjectMapper().writeValueAsString(event);
return MessageBuilder.withPayload(json)
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
.build();
} catch (JsonProcessingException e) {
fail(e.getMessage());
}
return null;
}
@TestConfiguration
static class EventProtoStuffTestConfig {
@Bean
public MessageConverter eventProtoBufConverter() {
return new EventProtoStuffMessageConverter();
}
}
}

View File

@@ -0,0 +1,150 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.event.remote;
import org.eclipse.hawkbit.repository.event.EventPublisherHolder;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.service.CancelTargetAssignmentServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.MultiActionAssignServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAssignDistributionSetServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAttributesRequestedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetCreatedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetDeletedServiceEvent;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.context.ApplicationEventPublisher;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
import java.util.List;
import java.util.Set;
class ServiceEventsTest {
private StreamBridge streamBridge;
private ApplicationEventPublisher delegate;
private ApplicationEventPublisher publisher;
@BeforeEach
void setUp() throws IllegalAccessException, NoSuchFieldException {
streamBridge = mock(StreamBridge.class);
delegate = mock(ApplicationEventPublisher.class);
EventPublisherHolder.getInstance().setApplicationEventPublisher(delegate);
EventPublisherHolder.getInstance().setStreamBridge(streamBridge);
publisher = EventPublisherHolder.getInstance().getEventPublisher();
// Set up fields via reflection
var remoteEventsEnabledField = EventPublisherHolder.class.getDeclaredField("remoteEventsEnabled");
remoteEventsEnabledField.setAccessible(true);
remoteEventsEnabledField.set(EventPublisherHolder.getInstance(), true);
var remoteServiceEventsEnabledField = EventPublisherHolder.class.getDeclaredField("remoteServiceEventsEnabled");
remoteServiceEventsEnabledField.setAccessible(true);
remoteServiceEventsEnabledField.set(EventPublisherHolder.getInstance(), true);
var fanoutChannelField = EventPublisherHolder.class.getDeclaredField("fanoutEventChannel");
fanoutChannelField.setAccessible(true);
fanoutChannelField.set(EventPublisherHolder.getInstance(), "fanout");
var groupChannelField = EventPublisherHolder.class.getDeclaredField("serviceEventChannel");
groupChannelField.setAccessible(true);
groupChannelField.set(EventPublisherHolder.getInstance(), "group");
}
@Test
void testExpectedServiceEvents(){
var expected = Set.of(
TargetAssignDistributionSetEvent.class,
MultiActionAssignEvent.class,
MultiActionCancelEvent.class,
CancelTargetAssignmentEvent.class,
TargetDeletedEvent.class,
TargetCreatedEvent.class,
TargetAttributesRequestedEvent.class
);
assertEquals(EventPublisherHolder.SERVICE_EVENTS, expected);
}
@Test
void testProcessingTargetAssignDistributionSetEventIsSent() {
TargetAssignDistributionSetEvent event = new TargetAssignDistributionSetEvent(mockAction());
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(TargetAssignDistributionSetServiceEvent.class));
}
@Test
void testProcessingTargetCreatedEventIsSent() {
TargetCreatedEvent event = new TargetCreatedEvent(mock(Target.class));
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(TargetCreatedServiceEvent.class));
}
@Test
void testProcessingTargetDeletedEventIsSent() {
TargetDeletedEvent event = new TargetDeletedEvent("testtenant", 1l, Target.class, "testControllerId", "address");
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(TargetDeletedServiceEvent.class));
}
@Test
void testProcessingTargetAttributesRequestedEventIsSent() {
TargetAttributesRequestedEvent event = new TargetAttributesRequestedEvent("testtenant", 1l, Target.class, "testControllerId","address");
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(TargetAttributesRequestedServiceEvent.class));
}
@Test
void testProcessingMultiActionAssignmentEventIsSent() {
MultiActionAssignEvent event = new MultiActionAssignEvent("testtenant", List.of(mockAction()));
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(MultiActionAssignServiceEvent.class));
}
@Test
void testCancelTargetAssignmentEventIsSent() {
CancelTargetAssignmentEvent event = new CancelTargetAssignmentEvent(mockAction());
publisher.publishEvent(event);
verify(streamBridge).send("fanout", event);
verify(streamBridge).send(eq("group"), any(CancelTargetAssignmentServiceEvent.class));
}
private Action mockAction() {
final Action actionMock = mock(Action.class);
final Target targetMock = mock(Target.class);
final DistributionSet distributionSetMock = mock(DistributionSet.class);
when(distributionSetMock.getId()).thenReturn(1L);
when(actionMock.getDistributionSet()).thenReturn(distributionSetMock);
when(actionMock.getId()).thenReturn(1l);
when(actionMock.getTenant()).thenReturn("DEFAULT");
when(actionMock.getTarget()).thenReturn(targetMock);
when(actionMock.getActionType()).thenReturn(Action.ActionType.SOFT);
when(targetMock.getControllerId()).thenReturn("target1");
return actionMock;
}
}

View File

@@ -9,7 +9,6 @@
#
### Debug & Monitor Eclipselink - START
logging.level.org.eclipse.persistence=ERROR
## Uncomment to see the debug of persistence, e.g. to see the generated SQLs
#logging.level.org.eclipse.persistence=DEBUG
@@ -56,5 +55,5 @@ hawkbit.repository.cluster.lock.refreshOnRemainMS=200
hawkbit.repository.cluster.lock.refreshOnRemainPercent=10
# reduce scheduler tic period to speed up tests
hawkbit.repository.cluster.lock.ticPeriodMS=10
# disable spring cloud bus for tests
spring.cloud.bus.enabled=false
org.eclipse.hawkbit.events.remote-enabled=false

View File

@@ -24,7 +24,6 @@ import org.eclipse.hawkbit.repository.artifact.ArtifactRepository;
import org.eclipse.hawkbit.repository.artifact.urlhandler.ArtifactUrlHandlerProperties;
import org.eclipse.hawkbit.repository.artifact.urlhandler.PropertyBasedArtifactUrlHandler;
import org.eclipse.hawkbit.cache.TenantAwareCacheManager;
import org.eclipse.hawkbit.event.BusProtoStuffMessageConverter;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.RolloutStatusCache;
import org.eclipse.hawkbit.repository.event.ApplicationEventFilter;
@@ -51,7 +50,6 @@ import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cloud.bus.ConditionalOnBusEnabled;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -63,7 +61,6 @@ import org.springframework.core.ResolvableType;
import org.springframework.data.domain.AuditorAware;
import org.springframework.integration.support.locks.DefaultLockRegistry;
import org.springframework.integration.support.locks.LockRegistry;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService;
@@ -181,7 +178,7 @@ public class TestConfiguration implements AsyncConfigurer {
}
@Bean
EventPublisherHolder eventBusHolder() {
EventPublisherHolder eventPublisherHolder() {
return EventPublisherHolder.getInstance();
}
@@ -208,15 +205,6 @@ public class TestConfiguration implements AsyncConfigurer {
return new RolloutTestApprovalStrategy();
}
/**
* @return the protostuff io message converter
*/
@Bean
@ConditionalOnBusEnabled
MessageConverter busProtoBufConverter() {
return new BusProtoStuffMessageConverter();
}
private static class FilterEnabledApplicationEventPublisher extends SimpleApplicationEventMulticaster {
private final ApplicationEventFilter applicationEventFilter;

View File

@@ -15,8 +15,12 @@ import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.Serial;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -27,10 +31,23 @@ import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionTimeoutException;
import org.eclipse.hawkbit.repository.event.remote.AbstractRemoteEvent;
import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent;
import org.eclipse.hawkbit.repository.event.remote.MultiActionAssignEvent;
import org.eclipse.hawkbit.repository.event.remote.MultiActionCancelEvent;
import org.eclipse.hawkbit.repository.event.remote.service.CancelTargetAssignmentServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.MultiActionAssignServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.MultiActionCancelServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAssignDistributionSetServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetAttributesRequestedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetCreatedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.service.TargetDeletedServiceEvent;
import org.eclipse.hawkbit.repository.event.remote.RemoteIdEvent;
import org.eclipse.hawkbit.repository.event.remote.RemoteTenantAwareEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
@@ -86,7 +103,50 @@ public class EventVerifier extends AbstractTestExecutionListener {
}
private Optional<Expect[]> getExpectationsFrom(final Method testMethod) {
return Optional.ofNullable(testMethod.getAnnotation(ExpectEvents.class)).map(ExpectEvents::value);
final Optional<Expect[]> expectedEvents = Optional.ofNullable(testMethod.getAnnotation(ExpectEvents.class)).map(ExpectEvents::value);
if (expectedEvents.isPresent()) {
List<Expect> modifiedEvents = new ArrayList<>(Arrays.asList(expectedEvents.get()));
for (Expect event : expectedEvents.get()) {
final Class<?> type = event.type();
if (type.isAssignableFrom(TargetCreatedEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(TargetCreatedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetDeletedEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(TargetDeletedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetAssignDistributionSetEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(TargetAssignDistributionSetServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(MultiActionAssignEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(MultiActionAssignServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(MultiActionCancelEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(MultiActionCancelServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(TargetAttributesRequestedEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(TargetAttributesRequestedServiceEvent.class, event.count()));
} else if (type.isAssignableFrom(CancelTargetAssignmentEvent.class)) {
modifiedEvents.add(toExpectServiceEvent(CancelTargetAssignmentServiceEvent.class, event.count()));
}
}
return Optional.of(modifiedEvents.toArray(new Expect[0]));
}
return Optional.empty();
}
private static Expect toExpectServiceEvent(final Class<?> clazz, final int count) {
return new Expect() {
@Override
public Class<? extends Annotation> annotationType() {
return Expect.class;
}
@Override
public Class<?> type() {
return clazz;
}
@Override
public int count() {
return count;
}
};
}
private void beforeTest(final TestContext testContext) {
@@ -129,12 +189,12 @@ public class EventVerifier extends AbstractTestExecutionListener {
testContext.getApplicationContext().getBean(ApplicationEventMulticaster.class).removeApplicationListener(eventCaptor);
}
private static class EventCaptor implements ApplicationListener<RemoteApplicationEvent> {
private static class EventCaptor implements ApplicationListener<AbstractRemoteEvent> {
private final ConcurrentHashMap<Class<?>, Integer> capturedEvents = new ConcurrentHashMap<>();
@Override
public void onApplicationEvent(final RemoteApplicationEvent event) {
public void onApplicationEvent(final AbstractRemoteEvent event) {
log.debug("Received event {}", event.getClass().getSimpleName());
if (ResetCounterMarkerEvent.class.isAssignableFrom(event.getClass())) {
@@ -170,13 +230,13 @@ public class EventVerifier extends AbstractTestExecutionListener {
}
}
private static final class ResetCounterMarkerEvent extends RemoteApplicationEvent {
private static final class ResetCounterMarkerEvent extends AbstractRemoteEvent {
@Serial
private static final long serialVersionUID = 1L;
private ResetCounterMarkerEvent() {
super(new Object(), "resetcounter", DEFAULT_DESTINATION_FACTORY.getDestination(null));
super("event-verifier");
}
}
}

View File

@@ -30,6 +30,8 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionFactory;
import org.eclipse.hawkbit.repository.artifact.ArtifactRepository;
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactStoreException;
import org.eclipse.hawkbit.repository.ArtifactManagement;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
@@ -52,8 +54,6 @@ import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TargetTagManagement;
import org.eclipse.hawkbit.repository.TargetTypeManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.artifact.ArtifactRepository;
import org.eclipse.hawkbit.repository.artifact.exception.ArtifactStoreException;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
@@ -78,7 +78,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.bus.ServiceMatcher;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.PageRequest;
@@ -181,8 +180,6 @@ public abstract class AbstractIntegrationTest {
@Autowired
protected TestdataFactory testdataFactory;
@Autowired(required = false)
protected ServiceMatcher serviceMatcher;
@Autowired
protected ApplicationEventPublisher eventPublisher;
private static final String ARTIFACT_DIRECTORY = createTempDir().getAbsolutePath() + "/" + randomString(20);

View File

@@ -78,7 +78,4 @@ hawkbit.server.security.dos.maxTargetsPerAutoAssignment=20
hawkbit.server.security.dos.maxActionsPerTarget=20
# Quota - END
# Properties that are managed by autoconfigure module at runtime and not available during test - END
# disable spring cloud bus for tests
spring.cloud.bus.enabled=false
# Properties that are managed by autoconfigure module at runtime and not available during test - END