Refactor RabbitMQ configuration (#2519)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-06-30 15:50:30 +03:00
committed by GitHub
parent 2a5f47df3f
commit cd2c68081f
24 changed files with 373 additions and 438 deletions

View File

@@ -32,6 +32,12 @@ jobs:
- 5672:5672
steps:
- name: Parameters
run: |
echo "Repository: ${{ inputs.repository }},"
echo "Ref: ${{ inputs.ref }},"
echo "Maven Properties: ${{ inputs.maven_properties }}"
- uses: actions/checkout@v4
with:
repository: ${{ inputs.repository }}

View File

@@ -22,6 +22,6 @@ jobs:
verify-hibernate:
uses: ./.github/workflows/reusable_workflow_verify.yaml
with:
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repository || github.repositor }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
maven_properties: '-Djpa.vendor=hibernate -Dlogging.level.org.hibernate.collection.spi.AbstractPersistentCollection=ERROR'

View File

@@ -22,5 +22,5 @@ jobs:
verify:
uses: ./.github/workflows/reusable_workflow_verify.yaml
with:
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repository || github.repositor }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}

View File

@@ -38,13 +38,6 @@ hawkbit.server.ddi.security.authentication.gatewaytoken.enabled=false
# Optional events
hawkbit.server.repository.publish-target-poll-event=false
## Configuration for DMF/RabbitMQ integration
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
# Enable CORS and specify the allowed origins:
#hawkbit.server.security.cors.enabled=true
#hawkbit.server.security.cors.allowedOrigins=http://localhost
@@ -57,11 +50,7 @@ hawkbit.lock=inMemory
# Disable discovery client of spring-cloud-commons
spring.cloud.discovery.enabled=false
# Enable communication between services
spring.cloud.bus.enabled=true
spring.cloud.bus.ack.enabled=false
spring.cloud.bus.refresh.enabled=false
spring.cloud.bus.env.enabled=false
# Configure communication between services
endpoints.spring.cloud.bus.refresh.enabled=false
endpoints.spring.cloud.bus.env.enabled=false
spring.cloud.stream.bindings.springCloudBusInput.group=ddi-server

View File

@@ -19,9 +19,9 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(properties = { "hawkbit.dmf.rabbitmq.enabled=false" })
@SpringBootTest
@ExtendWith(SharedSqlTestDatabaseExtension.class)
public abstract class AbstractSecurityTest {
abstract class AbstractSecurityTest {
protected MockMvc mvc;
@Autowired

View File

@@ -16,14 +16,14 @@ import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.TestPropertySource;
@TestPropertySource(properties = {
"spring.flyway.enabled=true", // if hibernate is used there could be db inconsistencies when executing tests with and without flyway
"hawkbit.server.security.allowedHostNames=localhost",
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html" })
/**
* Feature: Integration Test - Security<br/>
* Story: Allowed Host Names
*/
@TestPropertySource(properties = {
"spring.flyway.enabled=true", // if hibernate is used there could be db inconsistencies when executing tests with and without flyway
"hawkbit.server.security.allowedHostNames=localhost",
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html" })
class AllowedHostNamesTest extends AbstractSecurityTest {
/**

View File

@@ -1,324 +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
*/
package org.eclipse.hawkbit.amqp;
import java.sql.SQLException;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandler;
import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler;
import org.springframework.amqp.rabbit.listener.FatalExceptionStrategy;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.bus.ServiceMatcher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.ErrorHandler;
/**
* Spring configuration for AMQP based DMF communication for indirect device integration.
*/
@Slf4j
@EnableConfigurationProperties({ AmqpProperties.class, AmqpDeadletterProperties.class })
@ConditionalOnProperty(prefix = "hawkbit.dmf.rabbitmq", name = "enabled", matchIfMissing = true)
@PropertySource("classpath:/hawkbit-dmf-defaults.properties")
public class AmqpConfiguration {
private final AmqpProperties amqpProperties;
private final AmqpDeadletterProperties amqpDeadletterProperties;
private final ConnectionFactory rabbitConnectionFactory;
private ServiceMatcher serviceMatcher;
public AmqpConfiguration(final AmqpProperties amqpProperties, final AmqpDeadletterProperties amqpDeadletterProperties,
final ConnectionFactory rabbitConnectionFactory) {
this.amqpProperties = amqpProperties;
this.amqpDeadletterProperties = amqpDeadletterProperties;
this.rabbitConnectionFactory = rabbitConnectionFactory;
}
@Autowired(required = false) // spring setter injection
public void setServiceMatcher(final ServiceMatcher serviceMatcher) {
this.serviceMatcher = serviceMatcher;
}
@Bean
public FatalExceptionStrategy sqlFatalSQLExceptionStrategy(final AmqpProperties amqpProperties) {
return new SqlFatalExceptionStrategy(amqpProperties.getFatalSqlExceptionPolicy());
}
/**
* Creates a custom error handler bean.
*
* @param fatalExceptionStrategies list of {@link FatalExceptionStrategy} handlers. isFatal will be called for causes,
* up to the first fatal, so the implementation don't need to iterate over the causes.
* @return the delegating error handler bean
*/
@Bean
@ConditionalOnMissingBean
public ErrorHandler errorHandler(
final List<FatalExceptionStrategy> fatalExceptionStrategies,
@Value("${hawkbit.dmf.rabbitmq.fatal-exception-types:}") final List<String> fatalExceptionTypes) {
return new ConditionalRejectingErrorHandler(new RequeueExceptionStrategy(fatalExceptionStrategies, fatalExceptionTypes));
}
/**
* Create a {@link RabbitAdmin} and ignore declaration exceptions.
* {@link RabbitAdmin#setIgnoreDeclarationExceptions(boolean)}
*
* @return the bean
*/
@Bean
public RabbitAdmin rabbitAdmin() {
final RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitConnectionFactory);
rabbitAdmin.setIgnoreDeclarationExceptions(true);
return rabbitAdmin;
}
/**
* @return {@link RabbitTemplate} with automatic retry, published confirms and {@link Jackson2JsonMessageConverter}.
*/
@Bean
public RabbitTemplate rabbitTemplate() {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(rabbitConnectionFactory);
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
final RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(new ExponentialBackOffPolicy());
rabbitTemplate.setRetryTemplate(retryTemplate);
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.debug("Message with {} confirmed by broker.", correlationData);
} else {
log.error("Broker is unable to handle message with {} : {}", correlationData, cause);
}
});
return rabbitTemplate;
}
/**
* Create the DMF API receiver queue for retrieving DMF messages.
*
* @return the receiver queue
*/
@Bean
public Queue dmfReceiverQueue() {
return new Queue(
amqpProperties.getReceiverQueue(),
true, false, false,
amqpDeadletterProperties.getDeadLetterExchangeArgs(amqpProperties.getDeadLetterExchange()));
}
/**
* Create the DMF API receiver queue for authentication requests called by 3rd
* party artifact storages for download authorization by devices.
*
* @return the receiver queue
*/
@Bean
public Queue authenticationReceiverQueue() {
return QueueBuilder.nonDurable(amqpProperties.getAuthenticationReceiverQueue())
.autoDelete()
.withArguments(getTTLMaxArgsAuthenticationQueue())
.build();
}
/**
* Create DMF exchange.
*
* @return the fanout exchange
*/
@Bean
public FanoutExchange dmfSenderExchange() {
return new FanoutExchange(AmqpSettings.DMF_EXCHANGE);
}
/**
* Create the Binding {@link AmqpConfiguration#dmfReceiverQueue()} to
* {@link AmqpConfiguration#dmfSenderExchange()}.
*
* @return the binding and create the queue and exchange
*/
@Bean
public Binding bindDmfSenderExchangeToDmfQueue() {
return BindingBuilder.bind(dmfReceiverQueue()).to(dmfSenderExchange());
}
/**
* Create dead letter queue.
*
* @return the queue
*/
@Bean
public Queue deadLetterQueue() {
return amqpDeadletterProperties.createDeadletterQueue(amqpProperties.getDeadLetterQueue());
}
/**
* Create the dead letter fanout exchange.
*
* @return the fanout exchange
*/
@Bean
public FanoutExchange deadLetterExchange() {
return new FanoutExchange(amqpProperties.getDeadLetterExchange());
}
/**
* Create the Binding deadLetterQueue to deadLetterExchange.
*
* @return the binding
*/
@Bean
public Binding bindDeadLetterQueueToDeadLetterExchange() {
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange());
}
/**
* Create AMQP handler service bean.
*
* @param rabbitTemplate for converting messages
* @param amqpMessageDispatcherService to sending events to DMF client
* @param controllerManagement for target repo access
* @param entityFactory to create entities
* @return handler service bean
*/
@Bean
@ConditionalOnMissingBean
public AmqpMessageHandlerService amqpMessageHandlerService(
final RabbitTemplate rabbitTemplate,
final AmqpMessageDispatcherService amqpMessageDispatcherService,
final ControllerManagement controllerManagement, final EntityFactory entityFactory,
final SystemSecurityContext systemSecurityContext,
final TenantConfigurationManagement tenantConfigurationManagement,
final ConfirmationManagement confirmationManagement) {
return new AmqpMessageHandlerService(
rabbitTemplate, amqpMessageDispatcherService, controllerManagement,
entityFactory, systemSecurityContext, tenantConfigurationManagement, confirmationManagement);
}
/**
* Create default amqp sender service bean.
*
* @return the default amqp sender service bean
*/
@Bean
@ConditionalOnMissingBean
public AmqpMessageSenderService amqpSenderServiceBean() {
return new DefaultAmqpMessageSenderService(rabbitTemplate());
}
/**
* Create RabbitListenerContainerFactory bean if no listenerContainerFactory bean found
*
* @return RabbitListenerContainerFactory bean
*/
@Bean
@ConditionalOnMissingBean(name = "listenerContainerFactory")
public RabbitListenerContainerFactory<SimpleMessageListenerContainer> listenerContainerFactory(
final SimpleRabbitListenerContainerFactoryConfigurer configurer, final ErrorHandler errorHandler) {
final ConfigurableRabbitListenerContainerFactory factory = new ConfigurableRabbitListenerContainerFactory(
amqpProperties.isMissingQueuesFatal(), amqpProperties.getDeclarationRetries(), errorHandler);
configurer.configure(factory, rabbitConnectionFactory);
return factory;
}
@Bean
@ConditionalOnMissingBean(AmqpMessageDispatcherService.class)
AmqpMessageDispatcherService amqpMessageDispatcherService(
final RabbitTemplate rabbitTemplate,
final AmqpMessageSenderService amqpSenderService, final ArtifactUrlHandler artifactUrlHandler,
final SystemSecurityContext systemSecurityContext, final SystemManagement systemManagement,
final TargetManagement targetManagement, final DistributionSetManagement distributionSetManagement,
final SoftwareModuleManagement softwareModuleManagement, final DeploymentManagement deploymentManagement,
final TenantConfigurationManagement tenantConfigurationManagement) {
return new AmqpMessageDispatcherService(rabbitTemplate, amqpSenderService, artifactUrlHandler,
systemSecurityContext, systemManagement, targetManagement, serviceMatcher, distributionSetManagement,
softwareModuleManagement, deploymentManagement, tenantConfigurationManagement);
}
private static Map<String, Object> getTTLMaxArgsAuthenticationQueue() {
final Map<String, Object> args = new HashMap<>(2);
args.put("x-message-ttl", Duration.ofSeconds(30).toMillis());
args.put("x-max-length", 1_000);
return args;
}
@ToString
private static class SqlFatalExceptionStrategy implements FatalExceptionStrategy {
private final boolean fatalByDefault;
private final List<Integer> unlessErrorCodeIn;
private final List<String> unlessSqlStateIn;
private final List<Pattern> unlessMessageMatches;
public SqlFatalExceptionStrategy(final AmqpProperties.FatalSqlExceptionPolicy fatalSqlExceptions) {
this.fatalByDefault = fatalSqlExceptions.isByDefault();
this.unlessErrorCodeIn = fatalSqlExceptions.getUnlessErrorCodeIn();
this.unlessSqlStateIn = fatalSqlExceptions.getUnlessSqlStateIn();
this.unlessMessageMatches = fatalSqlExceptions.getUnlessMessageMatches();
}
@Override
public boolean isFatal(final Throwable t) {
if (t instanceof SQLException sqlException) {
if (unlessErrorCodeIn.contains(sqlException.getErrorCode())) {
return !fatalByDefault;
} else if (unlessSqlStateIn.contains(sqlException.getSQLState())) {
return !fatalByDefault;
} else {
for (final Pattern pattern : unlessMessageMatches) {
if (pattern.matcher(sqlException.getMessage()).matches()) {
return !fatalByDefault;
}
}
return fatalByDefault;
}
}
return false;
}
}
}

View File

@@ -25,11 +25,6 @@ public class AmqpProperties {
private static final int DEFAULT_QUEUE_DECLARATION_RETRIES = 50;
/**
* Enable DMF API based on AMQP 0.9
*/
private boolean enabled = true;
/**
* DMF API dead letter queue.
*/

View File

@@ -9,14 +9,319 @@
*/
package org.eclipse.hawkbit.amqp;
import java.sql.SQLException;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandler;
import org.eclipse.hawkbit.dmf.amqp.api.AmqpSettings;
import org.eclipse.hawkbit.repository.ConfirmationManagement;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SystemManagement;
import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler;
import org.springframework.amqp.rabbit.listener.FatalExceptionStrategy;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.bus.ServiceMatcher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.ErrorHandler;
/**
* Enable Device Management Federation API.
* Spring configuration for AMQP based DMF communication for indirect device integration.
*/
@Configuration
@Slf4j
@ComponentScan
@Import(AmqpConfiguration.class)
public class DmfApiConfiguration {}
@EnableConfigurationProperties({ AmqpProperties.class, AmqpDeadletterProperties.class })
@ConditionalOnProperty(prefix = "hawkbit.dmf", name = "enabled", matchIfMissing = true)
@PropertySource("classpath:/hawkbit-dmf-defaults.properties")
public class DmfApiConfiguration {
private final AmqpProperties amqpProperties;
private final AmqpDeadletterProperties amqpDeadletterProperties;
private final ConnectionFactory rabbitConnectionFactory;
private ServiceMatcher serviceMatcher;
public DmfApiConfiguration(
final AmqpProperties amqpProperties, final AmqpDeadletterProperties amqpDeadletterProperties,
final ConnectionFactory rabbitConnectionFactory) {
this.amqpProperties = amqpProperties;
this.amqpDeadletterProperties = amqpDeadletterProperties;
this.rabbitConnectionFactory = rabbitConnectionFactory;
}
@Autowired(required = false) // spring setter injection
public void setServiceMatcher(final ServiceMatcher serviceMatcher) {
this.serviceMatcher = serviceMatcher;
}
@Bean
public FatalExceptionStrategy sqlFatalSQLExceptionStrategy(final AmqpProperties amqpProperties) {
return new SqlFatalExceptionStrategy(amqpProperties.getFatalSqlExceptionPolicy());
}
/**
* Creates a custom error handler bean.
*
* @param fatalExceptionStrategies list of {@link FatalExceptionStrategy} handlers. isFatal will be called for causes,
* up to the first fatal, so the implementation don't need to iterate over the causes.
* @return the delegating error handler bean
*/
@Bean
@ConditionalOnMissingBean
public ErrorHandler errorHandler(
final List<FatalExceptionStrategy> fatalExceptionStrategies,
@Value("${hawkbit.dmf.rabbitmq.fatal-exception-types:}") final List<String> fatalExceptionTypes) {
return new ConditionalRejectingErrorHandler(new RequeueExceptionStrategy(fatalExceptionStrategies, fatalExceptionTypes));
}
/**
* Create a {@link RabbitAdmin} and ignore declaration exceptions.
* {@link RabbitAdmin#setIgnoreDeclarationExceptions(boolean)}
*
* @return the bean
*/
@Bean
public RabbitAdmin rabbitAdmin() {
final RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitConnectionFactory);
rabbitAdmin.setIgnoreDeclarationExceptions(true);
return rabbitAdmin;
}
/**
* @return {@link RabbitTemplate} with automatic retry, published confirms and {@link Jackson2JsonMessageConverter}.
*/
@Bean
public RabbitTemplate rabbitTemplate() {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(rabbitConnectionFactory);
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
final RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setBackOffPolicy(new ExponentialBackOffPolicy());
rabbitTemplate.setRetryTemplate(retryTemplate);
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.debug("Message with {} confirmed by broker.", correlationData);
} else {
log.error("Broker is unable to handle message with {} : {}", correlationData, cause);
}
});
return rabbitTemplate;
}
/**
* Create the DMF API receiver queue for retrieving DMF messages.
*
* @return the receiver queue
*/
@Bean
public Queue dmfReceiverQueue() {
return new Queue(
amqpProperties.getReceiverQueue(),
true, false, false,
amqpDeadletterProperties.getDeadLetterExchangeArgs(amqpProperties.getDeadLetterExchange()));
}
/**
* Create the DMF API receiver queue for authentication requests called by 3rd
* party artifact storages for download authorization by devices.
*
* @return the receiver queue
*/
@Bean
public Queue authenticationReceiverQueue() {
return QueueBuilder.nonDurable(amqpProperties.getAuthenticationReceiverQueue())
.autoDelete()
.withArguments(getTTLMaxArgsAuthenticationQueue())
.build();
}
/**
* Create DMF exchange.
*
* @return the fanout exchange
*/
@Bean
public FanoutExchange dmfSenderExchange() {
return new FanoutExchange(AmqpSettings.DMF_EXCHANGE);
}
/**
* Create the Binding {@link DmfApiConfiguration#dmfReceiverQueue()} to
* {@link DmfApiConfiguration#dmfSenderExchange()}.
*
* @return the binding and create the queue and exchange
*/
@Bean
public Binding bindDmfSenderExchangeToDmfQueue() {
return BindingBuilder.bind(dmfReceiverQueue()).to(dmfSenderExchange());
}
/**
* Create dead letter queue.
*
* @return the queue
*/
@Bean
public Queue deadLetterQueue() {
return amqpDeadletterProperties.createDeadletterQueue(amqpProperties.getDeadLetterQueue());
}
/**
* Create the dead letter fanout exchange.
*
* @return the fanout exchange
*/
@Bean
public FanoutExchange deadLetterExchange() {
return new FanoutExchange(amqpProperties.getDeadLetterExchange());
}
/**
* Create the Binding deadLetterQueue to deadLetterExchange.
*
* @return the binding
*/
@Bean
public Binding bindDeadLetterQueueToDeadLetterExchange() {
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange());
}
/**
* Create AMQP handler service bean.
*
* @param rabbitTemplate for converting messages
* @param amqpMessageDispatcherService to sending events to DMF client
* @param controllerManagement for target repo access
* @param entityFactory to create entities
* @return handler service bean
*/
@Bean
@ConditionalOnMissingBean
public AmqpMessageHandlerService amqpMessageHandlerService(
final RabbitTemplate rabbitTemplate,
final AmqpMessageDispatcherService amqpMessageDispatcherService,
final ControllerManagement controllerManagement, final EntityFactory entityFactory,
final SystemSecurityContext systemSecurityContext,
final TenantConfigurationManagement tenantConfigurationManagement,
final ConfirmationManagement confirmationManagement) {
return new AmqpMessageHandlerService(
rabbitTemplate, amqpMessageDispatcherService, controllerManagement,
entityFactory, systemSecurityContext, tenantConfigurationManagement, confirmationManagement);
}
/**
* Create default amqp sender service bean.
*
* @return the default amqp sender service bean
*/
@Bean
@ConditionalOnMissingBean
public AmqpMessageSenderService amqpSenderServiceBean() {
return new DefaultAmqpMessageSenderService(rabbitTemplate());
}
/**
* Create RabbitListenerContainerFactory bean if no listenerContainerFactory bean found
*
* @return RabbitListenerContainerFactory bean
*/
@Bean
@ConditionalOnMissingBean(name = "listenerContainerFactory")
public RabbitListenerContainerFactory<SimpleMessageListenerContainer> listenerContainerFactory(
final SimpleRabbitListenerContainerFactoryConfigurer configurer, final ErrorHandler errorHandler) {
final ConfigurableRabbitListenerContainerFactory factory = new ConfigurableRabbitListenerContainerFactory(
amqpProperties.isMissingQueuesFatal(), amqpProperties.getDeclarationRetries(), errorHandler);
configurer.configure(factory, rabbitConnectionFactory);
return factory;
}
@Bean
@ConditionalOnMissingBean(AmqpMessageDispatcherService.class)
AmqpMessageDispatcherService amqpMessageDispatcherService(
final RabbitTemplate rabbitTemplate,
final AmqpMessageSenderService amqpSenderService, final ArtifactUrlHandler artifactUrlHandler,
final SystemSecurityContext systemSecurityContext, final SystemManagement systemManagement,
final TargetManagement targetManagement, final DistributionSetManagement distributionSetManagement,
final SoftwareModuleManagement softwareModuleManagement, final DeploymentManagement deploymentManagement,
final TenantConfigurationManagement tenantConfigurationManagement) {
return new AmqpMessageDispatcherService(rabbitTemplate, amqpSenderService, artifactUrlHandler,
systemSecurityContext, systemManagement, targetManagement, serviceMatcher, distributionSetManagement,
softwareModuleManagement, deploymentManagement, tenantConfigurationManagement);
}
private static Map<String, Object> getTTLMaxArgsAuthenticationQueue() {
final Map<String, Object> args = new HashMap<>(2);
args.put("x-message-ttl", Duration.ofSeconds(30).toMillis());
args.put("x-max-length", 1_000);
return args;
}
@ToString
private static class SqlFatalExceptionStrategy implements FatalExceptionStrategy {
private final boolean fatalByDefault;
private final List<Integer> unlessErrorCodeIn;
private final List<String> unlessSqlStateIn;
private final List<Pattern> unlessMessageMatches;
public SqlFatalExceptionStrategy(final AmqpProperties.FatalSqlExceptionPolicy fatalSqlExceptions) {
this.fatalByDefault = fatalSqlExceptions.isByDefault();
this.unlessErrorCodeIn = fatalSqlExceptions.getUnlessErrorCodeIn();
this.unlessSqlStateIn = fatalSqlExceptions.getUnlessSqlStateIn();
this.unlessMessageMatches = fatalSqlExceptions.getUnlessMessageMatches();
}
@Override
public boolean isFatal(final Throwable t) {
if (t instanceof SQLException sqlException) {
if (unlessErrorCodeIn.contains(sqlException.getErrorCode())) {
return !fatalByDefault;
} else if (unlessSqlStateIn.contains(sqlException.getSQLState())) {
return !fatalByDefault;
} else {
for (final Pattern pattern : unlessMessageMatches) {
if (pattern.matcher(sqlException.getMessage()).matches()) {
return !fatalByDefault;
}
}
return fatalByDefault;
}
}
return false;
}
}
}

View File

@@ -24,14 +24,6 @@ logging.pattern.console=%clr(%d{${logging.pattern.dateformat:yyyy-MM-dd'T'HH:mm:
# Optional events
hawkbit.server.repository.publish-target-poll-event=false
## Configuration for DMF/RabbitMQ integration
hawkbit.dmf.rabbitmq.enabled=true
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.main.web-application-type=none
hawkbit.server.security.dos.filter.enabled=false
@@ -43,11 +35,7 @@ hawkbit.lock=inMemory
# Disable discovery client of spring-cloud-commons
spring.cloud.discovery.enabled=false
# Enable communication between services
spring.cloud.bus.enabled=true
spring.cloud.bus.ack.enabled=false
spring.cloud.bus.refresh.enabled=false
spring.cloud.bus.env.enabled=false
# Configure communication between services
endpoints.spring.cloud.bus.refresh.enabled=false
endpoints.spring.cloud.bus.env.enabled=false
spring.cloud.stream.bindings.springCloudBusInput.group=dmf-server

View File

@@ -37,24 +37,13 @@ server.servlet.encoding.force=true
# Optional events
hawkbit.server.repository.publish-target-poll-event=false
## Configuration for DMF/RabbitMQ integration
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
# Enable CORS and specify the allowed origins:
#hawkbit.server.security.cors.enabled=true
#hawkbit.server.security.cors.allowedOrigins=http://localhost
# Disable discovery client of spring-cloud-commons
spring.cloud.discovery.enabled=false
# Enable communication between services
spring.cloud.bus.enabled=true
spring.cloud.bus.ack.enabled=false
spring.cloud.bus.refresh.enabled=false
spring.cloud.bus.env.enabled=false
# Configure communication between services
endpoints.spring.cloud.bus.refresh.enabled=false
endpoints.spring.cloud.bus.env.enabled=false
spring.cloud.stream.bindings.springCloudBusInput.group=mgmt-server

View File

@@ -20,9 +20,9 @@ import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(properties = { "hawkbit.dmf.rabbitmq.enabled=false" })
@SpringBootTest
@ExtendWith(SharedSqlTestDatabaseExtension.class)
public abstract class AbstractSecurityTest {
abstract class AbstractSecurityTest {
protected MockMvc mvc;
@Autowired

View File

@@ -16,13 +16,13 @@ import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.TestPropertySource;
@TestPropertySource(properties = {
"hawkbit.server.security.allowedHostNames=localhost",
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html" })
/**
* Feature: Integration Test - Security<br/>
* Story: Allowed Host Names
*/
@TestPropertySource(properties = {
"hawkbit.server.security.allowedHostNames=localhost",
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html" })
class AllowedHostNamesTest extends AbstractSecurityTest {
/**

View File

@@ -22,19 +22,18 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.servlet.ResultActions;
/**
* Feature: Integration Test - Security<br/>
* Story: CORS
*/
@SpringBootTest(
properties = {
"hawkbit.dmf.rabbitmq.enabled=false",
"hawkbit.server.security.cors.enabled=true",
"hawkbit.server.security.cors.allowedOrigins=" +
CorsTest.ALLOWED_ORIGIN_FIRST + "," +
CorsTest.ALLOWED_ORIGIN_SECOND,
"hawkbit.server.security.cors.exposedHeaders=" +
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN })
/**
* Feature: Integration Test - Security<br/>
* Story: CORS
*/
class CorsTest extends AbstractSecurityTest {
static final String ALLOWED_ORIGIN_FIRST = "http://test.first.origin";

View File

@@ -26,12 +26,6 @@ The Management API can be accessed via http://localhost:8080/rest/v1
Clustering in hawkBit is based on _Spring Cloud Bus_. It is not enabled in the example app by default.
Add to your `application.properties` :
```properties
spring.cloud.bus.enabled=true
```
Add to your `pom.xml` :
```xml

View File

@@ -45,12 +45,14 @@ hawkbit.cache.global.ttl=0
# Optional events
hawkbit.server.repository.publish-target-poll-event=false
## Configuration for DMF/RabbitMQ integration
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
## Disable RabbitMQ auto configuration. Comment it to enable RabbitMQ support.
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
## Configuration Spring Bus (disabled by default) - no cluster support. To enable it, enable RabbitMQ (see above)
## and comment the line (spring.cloud.bus.enabled=false) or set spring.cloud.bus.enabled=true
spring.cloud.bus.enabled=false
## Disable DMF (by default) - no DMF support. To enable it, enable RabbitMQ (see above) and comment the line
## (hawkbit.dmf.rabbitmq.enabled=false) set hawkbit.dmf.rabbitmq.enabled=true
hawkbit.dmf.enabled=false
# Enable CORS and specify the allowed origins:
#hawkbit.server.security.cors.enabled=true

View File

@@ -20,9 +20,9 @@ import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(properties = { "hawkbit.dmf.rabbitmq.enabled=false" })
@SpringBootTest
@ExtendWith(SharedSqlTestDatabaseExtension.class)
public abstract class AbstractSecurityTest {
abstract class AbstractSecurityTest {
protected MockMvc mvc;
@Autowired

View File

@@ -16,14 +16,14 @@ import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.TestPropertySource;
@TestPropertySource(properties = {
"hawkbit.server.security.allowedHostNames=localhost",
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html"
})
/**
* Feature: Integration Test - Security<br/>
* Story: Allowed Host Names
*/
@TestPropertySource(properties = {
"hawkbit.server.security.allowedHostNames=localhost",
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html"
})
class AllowedHostNamesTest extends AbstractSecurityTest {
/**

View File

@@ -22,19 +22,18 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.test.web.servlet.ResultActions;
/**
* Feature: Integration Test - Security<br/>
* Story: CORS
*/
@SpringBootTest(
properties = {
"hawkbit.dmf.rabbitmq.enabled=false",
"hawkbit.server.security.cors.enabled=true",
"hawkbit.server.security.cors.allowedOrigins=" +
CorsTest.ALLOWED_ORIGIN_FIRST + "," +
CorsTest.ALLOWED_ORIGIN_SECOND,
"hawkbit.server.security.cors.exposedHeaders=" +
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN })
/**
* Feature: Integration Test - Security<br/>
* Story: CORS
*/
class CorsTest extends AbstractSecurityTest {
static final String ALLOWED_ORIGIN_FIRST = "http://test.first.origin";

View File

@@ -9,7 +9,7 @@
#
# Spring cloud bus and stream
spring.cloud.bus.enabled=false
spring.cloud.bus.enabled=true
# Disable Cloud Bus default events
spring.cloud.bus.env.enabled=false
spring.cloud.bus.ack.enabled=false

View File

@@ -8,16 +8,7 @@
# SPDX-License-Identifier: EPL-2.0
#
## Configuration for local RabbitMQ integration
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtualHost=/
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.dynamic=true
## Configuration for sdk dmf
hawkbit.sdk.dmf.enabled=true
## Configuration for SDK DMF
hawkbit.sdk.dmf.amqp.receiverConnectorQueueFromSp=sdk_receiver
hawkbit.sdk.dmf.amqp.senderForSpExchange=sdk.replyTo

View File

@@ -4,7 +4,7 @@ parent: Guides
weight: 31
---
In this guide we describe how to run a full featured hawkBit setup based on a production ready infrastructure. It is
In this guide we describe how to run a full-featured hawkBit setup based on a production ready infrastructure. It is
based on the hawkBit example modules and update server.
<!--more-->
@@ -20,7 +20,7 @@ This guide describes a target architecture that you will probably expect in a pr
- hawkBit [Update Server](https://github.com/eclipse-hawkbit/hawkbit/tree/master/hawkbit-monolith/hawkbit-update-server).
- [MariaDB](https://mariadb.org) for the repository.
- [RabbitMQ](https://www.rabbitmq.com) for DMF communication.
- [RabbitMQ](https://www.rabbitmq.com) for DMF communication (optional for monolith / single host deployment).
For testing, demonstration or integrations purposes you could also use hawkBit SDK:
- [hawkBit SDK Management API client](https://github.com/eclipse-hawkbit/hawkbit/blob/master/hawkbit-sdk/hawkbit-sdk-commons/src/main/java/org/eclipse/hawkbit/sdk/HawkbitClient.java).
@@ -50,17 +50,17 @@ spring.datasource.username=<YOUR_USER>
spring.datasource.password=<YOUR_PWD>
```
### Configure RabbitMQ connection settings for update server and device simulator (optional).
### Configure RabbitMQ connection settings for services (optional for monolith single host deployments).
We provide already defaults that should work with a standard Rabbit installation. Otherwise configure the following in
the `application.properties` of the two services:
We provide already defaults that should work with a standard Rabbit installation (spring boot RabbitProperties defaults).
Otherwise, configure the following in the `application.properties` of the services:
```properties
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.virtualHost=/
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=<Rabbit MQ host>
spring.rabbitmq.port=<Rabbit MQ port>
spring.rabbitmq.virtualHost=<virtual host to be used>
spring.rabbitmq.username=<YOUR_USER>
spring.rabbitmq.password=<YOUR_PWD>
```
### Adapt hostnames of demo simulator

View File

@@ -129,7 +129,12 @@
<dependencies>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-starter</artifactId>
<artifactId>hawkbit-ddi-starter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-mgmt-starter</artifactId>
<version>${project.version}</version>
</dependency>

View File

@@ -27,9 +27,6 @@ hawkbit.server.ddi.security.authentication.gatewaytoken.enabled=false
# Optional events
hawkbit.server.repository.publish-target-poll-event=false
## Configuration for DMF/RabbitMQ integration
hawkbit.dmf.rabbitmq.enabled=false
# Swagger Configuration
springdoc.api-docs.version=openapi_3_0
springdoc.show-oauth2-endpoints=true