Refactor RabbitMQ configuration (#2519)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -32,6 +32,12 @@ jobs:
|
|||||||
- 5672:5672
|
- 5672:5672
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Parameters
|
||||||
|
run: |
|
||||||
|
echo "Repository: ${{ inputs.repository }},"
|
||||||
|
echo "Ref: ${{ inputs.ref }},"
|
||||||
|
echo "Maven Properties: ${{ inputs.maven_properties }}"
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: ${{ inputs.repository }}
|
repository: ${{ inputs.repository }}
|
||||||
|
|||||||
2
.github/workflows/verify-hibernate.yaml
vendored
2
.github/workflows/verify-hibernate.yaml
vendored
@@ -22,6 +22,6 @@ jobs:
|
|||||||
verify-hibernate:
|
verify-hibernate:
|
||||||
uses: ./.github/workflows/reusable_workflow_verify.yaml
|
uses: ./.github/workflows/reusable_workflow_verify.yaml
|
||||||
with:
|
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 }}
|
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'
|
maven_properties: '-Djpa.vendor=hibernate -Dlogging.level.org.hibernate.collection.spi.AbstractPersistentCollection=ERROR'
|
||||||
2
.github/workflows/verify.yaml
vendored
2
.github/workflows/verify.yaml
vendored
@@ -22,5 +22,5 @@ jobs:
|
|||||||
verify:
|
verify:
|
||||||
uses: ./.github/workflows/reusable_workflow_verify.yaml
|
uses: ./.github/workflows/reusable_workflow_verify.yaml
|
||||||
with:
|
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 }}
|
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
|
||||||
@@ -38,13 +38,6 @@ hawkbit.server.ddi.security.authentication.gatewaytoken.enabled=false
|
|||||||
# Optional events
|
# Optional events
|
||||||
hawkbit.server.repository.publish-target-poll-event=false
|
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:
|
# Enable CORS and specify the allowed origins:
|
||||||
#hawkbit.server.security.cors.enabled=true
|
#hawkbit.server.security.cors.enabled=true
|
||||||
#hawkbit.server.security.cors.allowedOrigins=http://localhost
|
#hawkbit.server.security.cors.allowedOrigins=http://localhost
|
||||||
@@ -57,11 +50,7 @@ hawkbit.lock=inMemory
|
|||||||
|
|
||||||
# Disable discovery client of spring-cloud-commons
|
# Disable discovery client of spring-cloud-commons
|
||||||
spring.cloud.discovery.enabled=false
|
spring.cloud.discovery.enabled=false
|
||||||
# Enable communication between services
|
# Configure 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
|
|
||||||
endpoints.spring.cloud.bus.refresh.enabled=false
|
endpoints.spring.cloud.bus.refresh.enabled=false
|
||||||
endpoints.spring.cloud.bus.env.enabled=false
|
endpoints.spring.cloud.bus.env.enabled=false
|
||||||
spring.cloud.stream.bindings.springCloudBusInput.group=ddi-server
|
spring.cloud.stream.bindings.springCloudBusInput.group=ddi-server
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import org.springframework.test.web.servlet.MockMvc;
|
|||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
import org.springframework.web.context.WebApplicationContext;
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
|
||||||
@SpringBootTest(properties = { "hawkbit.dmf.rabbitmq.enabled=false" })
|
@SpringBootTest
|
||||||
@ExtendWith(SharedSqlTestDatabaseExtension.class)
|
@ExtendWith(SharedSqlTestDatabaseExtension.class)
|
||||||
public abstract class AbstractSecurityTest {
|
abstract class AbstractSecurityTest {
|
||||||
|
|
||||||
protected MockMvc mvc;
|
protected MockMvc mvc;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.test.context.TestPropertySource;
|
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/>
|
* Feature: Integration Test - Security<br/>
|
||||||
* Story: Allowed Host Names
|
* 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 {
|
class AllowedHostNamesTest extends AbstractSecurityTest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,11 +25,6 @@ public class AmqpProperties {
|
|||||||
|
|
||||||
private static final int DEFAULT_QUEUE_DECLARATION_RETRIES = 50;
|
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.
|
* DMF API dead letter queue.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,14 +9,319 @@
|
|||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.amqp;
|
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.ComponentScan;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
import org.springframework.context.annotation.Import;
|
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
|
@ComponentScan
|
||||||
@Import(AmqpConfiguration.class)
|
@EnableConfigurationProperties({ AmqpProperties.class, AmqpDeadletterProperties.class })
|
||||||
public class DmfApiConfiguration {}
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,14 +24,6 @@ logging.pattern.console=%clr(%d{${logging.pattern.dateformat:yyyy-MM-dd'T'HH:mm:
|
|||||||
# Optional events
|
# Optional events
|
||||||
hawkbit.server.repository.publish-target-poll-event=false
|
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
|
spring.main.web-application-type=none
|
||||||
hawkbit.server.security.dos.filter.enabled=false
|
hawkbit.server.security.dos.filter.enabled=false
|
||||||
|
|
||||||
@@ -43,11 +35,7 @@ hawkbit.lock=inMemory
|
|||||||
|
|
||||||
# Disable discovery client of spring-cloud-commons
|
# Disable discovery client of spring-cloud-commons
|
||||||
spring.cloud.discovery.enabled=false
|
spring.cloud.discovery.enabled=false
|
||||||
# Enable communication between services
|
# Configure 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
|
|
||||||
endpoints.spring.cloud.bus.refresh.enabled=false
|
endpoints.spring.cloud.bus.refresh.enabled=false
|
||||||
endpoints.spring.cloud.bus.env.enabled=false
|
endpoints.spring.cloud.bus.env.enabled=false
|
||||||
spring.cloud.stream.bindings.springCloudBusInput.group=dmf-server
|
spring.cloud.stream.bindings.springCloudBusInput.group=dmf-server
|
||||||
|
|||||||
@@ -37,24 +37,13 @@ server.servlet.encoding.force=true
|
|||||||
# Optional events
|
# Optional events
|
||||||
hawkbit.server.repository.publish-target-poll-event=false
|
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:
|
# Enable CORS and specify the allowed origins:
|
||||||
#hawkbit.server.security.cors.enabled=true
|
#hawkbit.server.security.cors.enabled=true
|
||||||
#hawkbit.server.security.cors.allowedOrigins=http://localhost
|
#hawkbit.server.security.cors.allowedOrigins=http://localhost
|
||||||
|
|
||||||
# Disable discovery client of spring-cloud-commons
|
# Disable discovery client of spring-cloud-commons
|
||||||
spring.cloud.discovery.enabled=false
|
spring.cloud.discovery.enabled=false
|
||||||
# Enable communication between services
|
# Configure 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
|
|
||||||
endpoints.spring.cloud.bus.refresh.enabled=false
|
endpoints.spring.cloud.bus.refresh.enabled=false
|
||||||
endpoints.spring.cloud.bus.env.enabled=false
|
endpoints.spring.cloud.bus.env.enabled=false
|
||||||
spring.cloud.stream.bindings.springCloudBusInput.group=mgmt-server
|
spring.cloud.stream.bindings.springCloudBusInput.group=mgmt-server
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
|
|||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
import org.springframework.web.context.WebApplicationContext;
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
|
||||||
@SpringBootTest(properties = { "hawkbit.dmf.rabbitmq.enabled=false" })
|
@SpringBootTest
|
||||||
@ExtendWith(SharedSqlTestDatabaseExtension.class)
|
@ExtendWith(SharedSqlTestDatabaseExtension.class)
|
||||||
public abstract class AbstractSecurityTest {
|
abstract class AbstractSecurityTest {
|
||||||
|
|
||||||
protected MockMvc mvc;
|
protected MockMvc mvc;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.test.context.TestPropertySource;
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
|
||||||
@TestPropertySource(properties = {
|
|
||||||
"hawkbit.server.security.allowedHostNames=localhost",
|
|
||||||
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html" })
|
|
||||||
/**
|
/**
|
||||||
* Feature: Integration Test - Security<br/>
|
* Feature: Integration Test - Security<br/>
|
||||||
* Story: Allowed Host Names
|
* Story: Allowed Host Names
|
||||||
*/
|
*/
|
||||||
|
@TestPropertySource(properties = {
|
||||||
|
"hawkbit.server.security.allowedHostNames=localhost",
|
||||||
|
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html" })
|
||||||
class AllowedHostNamesTest extends AbstractSecurityTest {
|
class AllowedHostNamesTest extends AbstractSecurityTest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,19 +22,18 @@ import org.springframework.boot.test.context.SpringBootTest;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.test.web.servlet.ResultActions;
|
import org.springframework.test.web.servlet.ResultActions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature: Integration Test - Security<br/>
|
||||||
|
* Story: CORS
|
||||||
|
*/
|
||||||
@SpringBootTest(
|
@SpringBootTest(
|
||||||
properties = {
|
properties = {
|
||||||
"hawkbit.dmf.rabbitmq.enabled=false",
|
|
||||||
"hawkbit.server.security.cors.enabled=true",
|
"hawkbit.server.security.cors.enabled=true",
|
||||||
"hawkbit.server.security.cors.allowedOrigins=" +
|
"hawkbit.server.security.cors.allowedOrigins=" +
|
||||||
CorsTest.ALLOWED_ORIGIN_FIRST + "," +
|
CorsTest.ALLOWED_ORIGIN_FIRST + "," +
|
||||||
CorsTest.ALLOWED_ORIGIN_SECOND,
|
CorsTest.ALLOWED_ORIGIN_SECOND,
|
||||||
"hawkbit.server.security.cors.exposedHeaders=" +
|
"hawkbit.server.security.cors.exposedHeaders=" +
|
||||||
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN })
|
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN })
|
||||||
/**
|
|
||||||
* Feature: Integration Test - Security<br/>
|
|
||||||
* Story: CORS
|
|
||||||
*/
|
|
||||||
class CorsTest extends AbstractSecurityTest {
|
class CorsTest extends AbstractSecurityTest {
|
||||||
|
|
||||||
static final String ALLOWED_ORIGIN_FIRST = "http://test.first.origin";
|
static final String ALLOWED_ORIGIN_FIRST = "http://test.first.origin";
|
||||||
|
|||||||
@@ -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.
|
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` :
|
Add to your `pom.xml` :
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
|
|||||||
@@ -45,12 +45,14 @@ hawkbit.cache.global.ttl=0
|
|||||||
# Optional events
|
# Optional events
|
||||||
hawkbit.server.repository.publish-target-poll-event=false
|
hawkbit.server.repository.publish-target-poll-event=false
|
||||||
|
|
||||||
## Configuration for DMF/RabbitMQ integration
|
## Disable RabbitMQ auto configuration. Comment it to enable RabbitMQ support.
|
||||||
spring.rabbitmq.username=guest
|
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
|
||||||
spring.rabbitmq.password=guest
|
## Configuration Spring Bus (disabled by default) - no cluster support. To enable it, enable RabbitMQ (see above)
|
||||||
spring.rabbitmq.virtual-host=/
|
## and comment the line (spring.cloud.bus.enabled=false) or set spring.cloud.bus.enabled=true
|
||||||
spring.rabbitmq.host=localhost
|
spring.cloud.bus.enabled=false
|
||||||
spring.rabbitmq.port=5672
|
## 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:
|
# Enable CORS and specify the allowed origins:
|
||||||
#hawkbit.server.security.cors.enabled=true
|
#hawkbit.server.security.cors.enabled=true
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
|
|||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
import org.springframework.web.context.WebApplicationContext;
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
|
||||||
@SpringBootTest(properties = { "hawkbit.dmf.rabbitmq.enabled=false" })
|
@SpringBootTest
|
||||||
@ExtendWith(SharedSqlTestDatabaseExtension.class)
|
@ExtendWith(SharedSqlTestDatabaseExtension.class)
|
||||||
public abstract class AbstractSecurityTest {
|
abstract class AbstractSecurityTest {
|
||||||
|
|
||||||
protected MockMvc mvc;
|
protected MockMvc mvc;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.test.context.TestPropertySource;
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
|
||||||
@TestPropertySource(properties = {
|
|
||||||
"hawkbit.server.security.allowedHostNames=localhost",
|
|
||||||
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html"
|
|
||||||
})
|
|
||||||
/**
|
/**
|
||||||
* Feature: Integration Test - Security<br/>
|
* Feature: Integration Test - Security<br/>
|
||||||
* Story: Allowed Host Names
|
* Story: Allowed Host Names
|
||||||
*/
|
*/
|
||||||
|
@TestPropertySource(properties = {
|
||||||
|
"hawkbit.server.security.allowedHostNames=localhost",
|
||||||
|
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html"
|
||||||
|
})
|
||||||
class AllowedHostNamesTest extends AbstractSecurityTest {
|
class AllowedHostNamesTest extends AbstractSecurityTest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,19 +22,18 @@ import org.springframework.boot.test.context.SpringBootTest;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.test.web.servlet.ResultActions;
|
import org.springframework.test.web.servlet.ResultActions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature: Integration Test - Security<br/>
|
||||||
|
* Story: CORS
|
||||||
|
*/
|
||||||
@SpringBootTest(
|
@SpringBootTest(
|
||||||
properties = {
|
properties = {
|
||||||
"hawkbit.dmf.rabbitmq.enabled=false",
|
|
||||||
"hawkbit.server.security.cors.enabled=true",
|
"hawkbit.server.security.cors.enabled=true",
|
||||||
"hawkbit.server.security.cors.allowedOrigins=" +
|
"hawkbit.server.security.cors.allowedOrigins=" +
|
||||||
CorsTest.ALLOWED_ORIGIN_FIRST + "," +
|
CorsTest.ALLOWED_ORIGIN_FIRST + "," +
|
||||||
CorsTest.ALLOWED_ORIGIN_SECOND,
|
CorsTest.ALLOWED_ORIGIN_SECOND,
|
||||||
"hawkbit.server.security.cors.exposedHeaders=" +
|
"hawkbit.server.security.cors.exposedHeaders=" +
|
||||||
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN })
|
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN })
|
||||||
/**
|
|
||||||
* Feature: Integration Test - Security<br/>
|
|
||||||
* Story: CORS
|
|
||||||
*/
|
|
||||||
class CorsTest extends AbstractSecurityTest {
|
class CorsTest extends AbstractSecurityTest {
|
||||||
|
|
||||||
static final String ALLOWED_ORIGIN_FIRST = "http://test.first.origin";
|
static final String ALLOWED_ORIGIN_FIRST = "http://test.first.origin";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
# Spring cloud bus and stream
|
# Spring cloud bus and stream
|
||||||
spring.cloud.bus.enabled=false
|
spring.cloud.bus.enabled=true
|
||||||
# Disable Cloud Bus default events
|
# Disable Cloud Bus default events
|
||||||
spring.cloud.bus.env.enabled=false
|
spring.cloud.bus.env.enabled=false
|
||||||
spring.cloud.bus.ack.enabled=false
|
spring.cloud.bus.ack.enabled=false
|
||||||
|
|||||||
@@ -8,16 +8,7 @@
|
|||||||
# SPDX-License-Identifier: EPL-2.0
|
# SPDX-License-Identifier: EPL-2.0
|
||||||
#
|
#
|
||||||
|
|
||||||
## Configuration for local RabbitMQ integration
|
## Configuration for SDK DMF
|
||||||
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
|
|
||||||
hawkbit.sdk.dmf.amqp.receiverConnectorQueueFromSp=sdk_receiver
|
hawkbit.sdk.dmf.amqp.receiverConnectorQueueFromSp=sdk_receiver
|
||||||
hawkbit.sdk.dmf.amqp.senderForSpExchange=sdk.replyTo
|
hawkbit.sdk.dmf.amqp.senderForSpExchange=sdk.replyTo
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ parent: Guides
|
|||||||
weight: 31
|
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.
|
based on the hawkBit example modules and update server.
|
||||||
|
|
||||||
<!--more-->
|
<!--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).
|
- hawkBit [Update Server](https://github.com/eclipse-hawkbit/hawkbit/tree/master/hawkbit-monolith/hawkbit-update-server).
|
||||||
- [MariaDB](https://mariadb.org) for the repository.
|
- [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:
|
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).
|
- [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>
|
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
|
We provide already defaults that should work with a standard Rabbit installation (spring boot RabbitProperties defaults).
|
||||||
the `application.properties` of the two services:
|
Otherwise, configure the following in the `application.properties` of the services:
|
||||||
|
|
||||||
```properties
|
```properties
|
||||||
spring.rabbitmq.host=localhost
|
spring.rabbitmq.host=<Rabbit MQ host>
|
||||||
spring.rabbitmq.port=5672
|
spring.rabbitmq.port=<Rabbit MQ port>
|
||||||
spring.rabbitmq.virtualHost=/
|
spring.rabbitmq.virtualHost=<virtual host to be used>
|
||||||
spring.rabbitmq.username=guest
|
spring.rabbitmq.username=<YOUR_USER>
|
||||||
spring.rabbitmq.password=guest
|
spring.rabbitmq.password=<YOUR_PWD>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Adapt hostnames of demo simulator
|
### Adapt hostnames of demo simulator
|
||||||
|
|||||||
@@ -129,7 +129,12 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.hawkbit</groupId>
|
<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>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ hawkbit.server.ddi.security.authentication.gatewaytoken.enabled=false
|
|||||||
# Optional events
|
# Optional events
|
||||||
hawkbit.server.repository.publish-target-poll-event=false
|
hawkbit.server.repository.publish-target-poll-event=false
|
||||||
|
|
||||||
## Configuration for DMF/RabbitMQ integration
|
|
||||||
hawkbit.dmf.rabbitmq.enabled=false
|
|
||||||
|
|
||||||
# Swagger Configuration
|
# Swagger Configuration
|
||||||
springdoc.api-docs.version=openapi_3_0
|
springdoc.api-docs.version=openapi_3_0
|
||||||
springdoc.show-oauth2-endpoints=true
|
springdoc.show-oauth2-endpoints=true
|
||||||
|
|||||||
Reference in New Issue
Block a user