Batch system config update (#1402)

* Added an endpoint for batch update of system configurations

Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com>

* batch db save

Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com>

* Review changes and added tests

Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com>

* Evict cache only if transaction is commited - such as @CacheEvict

Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com>

* refactoring

Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com>

* Using AfterTransactionCommitExecutor for cache eviction

Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com>

* Change request body

Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com>

---------

Signed-off-by: Denislav Prinov <denislav.prinov@bosch.com>
This commit is contained in:
Denislav Prinov
2023-08-02 11:15:27 +03:00
committed by GitHub
parent 1dc1bdbe94
commit fb30999d73
7 changed files with 201 additions and 28 deletions

View File

@@ -9,6 +9,7 @@
package org.eclipse.hawkbit.repository;
import java.io.Serializable;
import java.util.Map;
import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions;
import org.eclipse.hawkbit.repository.model.TenantConfiguration;
@@ -44,6 +45,22 @@ public interface TenantConfigurationManagement {
@PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION)
<T extends Serializable> TenantConfigurationValue<T> addOrUpdateConfiguration(String configurationKeyName, T value);
/**
* Adds or updates a specific configuration for a specific tenant.
*
*
* @param configurations
* map containing the key - value of the configuration
* @return map of all configuration values which were written into the database.
* @throws TenantConfigurationValidatorException
* if the {@code propertyType} and the value in general does not
* match the expected type and format defined by the Key
* @throws ConversionFailedException
* if the property cannot be converted to the given
*/
@PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION)
<T extends Serializable> Map<String, TenantConfigurationValue<T>> addOrUpdateConfiguration(Map<String, T> configurations);
/**
* Build the tenant configuration by the given key
*

View File

@@ -13,10 +13,16 @@ import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationPrope
import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValueChangeNotAllowedException;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
import org.eclipse.hawkbit.repository.jpa.model.JpaTenantConfiguration;
import org.eclipse.hawkbit.repository.model.TenantConfiguration;
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
@@ -26,6 +32,8 @@ import org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationVa
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationContext;
@@ -55,6 +63,12 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
@Autowired
private ApplicationContext applicationContext;
@Autowired
private CacheManager cacheManager;
@Autowired
private AfterTransactionCommitExecutor afterCommitExecutor;
private static final ConfigurableConversionService conversionService = new DefaultConversionService();
@Override
@@ -139,39 +153,68 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public <T extends Serializable> TenantConfigurationValue<T> addOrUpdateConfiguration(
final String configurationKeyName, final T value) {
return addOrUpdateConfiguration0(Collections.singletonMap(configurationKeyName, value)).values().iterator().next();
}
final TenantConfigurationKey configurationKey = tenantConfigurationProperties.fromKeyName(configurationKeyName);
@Override
@Transactional
@Retryable(include = {
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public <T extends Serializable> Map<String, TenantConfigurationValue<T>> addOrUpdateConfiguration(Map<String, T> configurations) {
// Register a callback to be invoked after the transaction is committed - for cache eviction
afterCommitExecutor.afterCommit(() -> {
Cache cache = cacheManager.getCache("tenantConfiguration");
if (cache != null) {
configurations.keySet().forEach(cache::evict);
}
});
if (!configurationKey.getDataType().isAssignableFrom(value.getClass())) {
throw new TenantConfigurationValidatorException(String.format(
"Cannot parse the value %s of type %s into the type %s defined by the configuration key.", value,
value.getClass(), configurationKey.getDataType()));
}
return addOrUpdateConfiguration0(configurations);
}
configurationKey.validate(applicationContext, value);
private <T extends Serializable> Map<String, TenantConfigurationValue<T>> addOrUpdateConfiguration0(Map<String, T> configurations) {
List<JpaTenantConfiguration> configurationList = new ArrayList<>();
configurations.forEach((configurationKeyName, value) -> {
final TenantConfigurationKey configurationKey = tenantConfigurationProperties.fromKeyName(configurationKeyName);
JpaTenantConfiguration tenantConfiguration = tenantConfigurationRepository
.findByKey(configurationKey.getKeyName());
if (!configurationKey.getDataType().isAssignableFrom(value.getClass())) {
throw new TenantConfigurationValidatorException(String.format(
"Cannot parse the value %s of type %s into the type %s defined by the configuration key.", value,
value.getClass(), configurationKey.getDataType()));
}
if (tenantConfiguration == null) {
tenantConfiguration = new JpaTenantConfiguration(configurationKey.getKeyName(), value.toString());
} else {
tenantConfiguration.setValue(value.toString());
}
configurationKey.validate(applicationContext, value);
assertValueChangeIsAllowed(configurationKeyName, tenantConfiguration);
JpaTenantConfiguration tenantConfiguration = tenantConfigurationRepository
.findByKey(configurationKey.getKeyName());
final JpaTenantConfiguration updatedTenantConfiguration = tenantConfigurationRepository
.save(tenantConfiguration);
if (tenantConfiguration == null) {
tenantConfiguration = new JpaTenantConfiguration(configurationKey.getKeyName(), value.toString());
} else {
tenantConfiguration.setValue(value.toString());
}
@SuppressWarnings("unchecked")
final Class<T> clazzT = (Class<T>) value.getClass();
assertValueChangeIsAllowed(configurationKeyName, tenantConfiguration);
configurationList.add(tenantConfiguration);
});
return TenantConfigurationValue.<T> builder().global(false).createdBy(updatedTenantConfiguration.getCreatedBy())
.createdAt(updatedTenantConfiguration.getCreatedAt())
.lastModifiedAt(updatedTenantConfiguration.getLastModifiedAt())
.lastModifiedBy(updatedTenantConfiguration.getLastModifiedBy())
.value(conversionService.convert(updatedTenantConfiguration.getValue(), clazzT)).build();
List<JpaTenantConfiguration> jpaTenantConfigurations = tenantConfigurationRepository
.saveAll(configurationList);
return jpaTenantConfigurations.stream().collect(Collectors.toMap(
JpaTenantConfiguration::getKey,
updatedTenantConfiguration -> {
@SuppressWarnings("unchecked")
final Class<T> clazzT = (Class<T>) configurations.get(updatedTenantConfiguration.getKey()).getClass();
return TenantConfigurationValue.<T>builder().global(false)
.createdBy(updatedTenantConfiguration.getCreatedBy())
.createdAt(updatedTenantConfiguration.getCreatedAt())
.lastModifiedAt(updatedTenantConfiguration.getLastModifiedAt())
.lastModifiedBy(updatedTenantConfiguration.getLastModifiedBy())
.value(conversionService.convert(updatedTenantConfiguration.getValue(), clazzT))
.build();
}));
}
/**

View File

@@ -73,7 +73,7 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
@Test
@Description("Tests that the tenant specific configuration can be updated")
public void updateTenantSpecifcConfiguration() {
public void updateTenantSpecificConfiguration() {
final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY;
final String value1 = "firstValue";
final String value2 = "secondValue";
@@ -89,6 +89,22 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
.isEqualTo(value2);
}
@Test
@Description("Tests that the tenant specific configuration can be batch updated")
public void batchUpdateTenantSpecificConfiguration() {
Map<String, Serializable> configuration = new HashMap<>() {{
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true);
}};
// add value first
tenantConfigurationManagement.addOrUpdateConfiguration(configuration);
assertThat(tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class).getValue())
.isEqualTo("token_123");
assertThat(tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, Boolean.class).getValue())
.isTrue();
}
@Test
@Description("Tests that the configuration value can be converted from String to Integer automatically")
public void storeAndUpdateTenantSpecificConfigurationAsBoolean() {
@@ -118,6 +134,26 @@ public class TenantConfigurationManagementTest extends AbstractJpaIntegrationTes
}
}
@Test
@Description("Tests that the get configuration throws exception in case the value is the wrong type")
public void batchWrongTenantConfigurationValueTypeThrowsException() {
Map<String, Serializable> configuration = new HashMap<>() {{
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123");
put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true);
put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, "wrong");
}};
try {
tenantConfigurationManagement.addOrUpdateConfiguration(configuration);
fail("should not have worked as type is wrong");
} catch (final TenantConfigurationValidatorException e) {
assertThat(tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class).getValue())
.isNotEqualTo("token_123");
assertThat(tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, Boolean.class).getValue())
.isNotEqualTo(true);
}
}
@Test
@Description("Tests that a deletion of a tenant specific configuration deletes it from the database.")
public void deleteConfigurationReturnNullConfiguration() {

View File

@@ -34,15 +34,15 @@ public class MgmtSystemTenantConfigurationValueRequest {
}
/**
* Sets the MgmtSystemTenantConfigurationValueRequest
* Sets the value of the MgmtSystemTenantConfigurationValueRequest
*
* @param value
*/
public void setValue(final Object value) {
if (!(value instanceof Serializable)) {
throw new IllegalArgumentException("The value muste be a instance of " + Serializable.class.getName());
throw new IllegalArgumentException("The value must be a instance of " + Serializable.class.getName());
}
this.value = (Serializable) value;
}
}

View File

@@ -8,6 +8,8 @@
*/
package org.eclipse.hawkbit.mgmt.rest.api;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import org.eclipse.hawkbit.mgmt.json.model.system.MgmtSystemTenantConfigurationValue;
@@ -19,6 +21,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* REST Resource for handling tenant specific configuration operations.
@@ -83,4 +86,20 @@ public interface MgmtTenantManagementRestApi {
ResponseEntity<MgmtSystemTenantConfigurationValue> updateTenantConfigurationValue(
@PathVariable("keyName") String keyName, MgmtSystemTenantConfigurationValueRequest configurationValueRest);
/**
* Handles the PUT request for updating a batch of tenant specific configurations
*
* @param configurationValueMap
* a Map of name - value pairs for the configurations
*
* @return if the given configurations values exists and could be get HTTP OK.
* In any failure the JsonResponseExceptionHandler is handling the
* response.
*/
@PutMapping(value = MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs", consumes = {
MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaTypes.HAL_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE })
ResponseEntity<List<MgmtSystemTenantConfigurationValue>> updateTenantConfiguration(
@RequestBody Map<String, Serializable> configurationValueMap);
}

View File

@@ -9,7 +9,9 @@
package org.eclipse.hawkbit.mgmt.rest.resource;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.hawkbit.mgmt.json.model.system.MgmtSystemTenantConfigurationValue;
import org.eclipse.hawkbit.mgmt.json.model.system.MgmtSystemTenantConfigurationValueRequest;
@@ -76,4 +78,22 @@ public class MgmtTenantManagementResource implements MgmtTenantManagementRestApi
return ResponseEntity.ok(MgmtTenantManagementMapper.toResponse(keyName, updatedValue));
}
@Override
public ResponseEntity<List<MgmtSystemTenantConfigurationValue>> updateTenantConfiguration(
Map<String, Serializable> configurationValueMap) {
boolean containsNull = configurationValueMap.keySet().stream()
.anyMatch(Objects::isNull);
if (containsNull) {
return ResponseEntity.badRequest().build();
}
Map<String, TenantConfigurationValue<Serializable>> tenantConfigurationValues = tenantConfigurationManagement
.addOrUpdateConfiguration(configurationValueMap);
return ResponseEntity.ok(tenantConfigurationValues.entrySet().stream().map(entry ->
MgmtTenantManagementMapper.toResponse(entry.getKey(), entry.getValue())).toList());
}
}

View File

@@ -32,6 +32,14 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg
private static final String KEY_MULTI_ASSIGNMENTS = "multi.assignments.enabled";
private static final String KEY_AUTO_CLOSE = "repository.actions.autoclose.enabled";
private static final String ROLLOUT_APPROVAL_ENABLED = "rollout.approval.enabled";
private static final String AUTHENTICATION_GATEWAYTOKEN_ENABLED = "authentication.gatewaytoken.enabled";
private static final String AUTHENTICATION_GATEWAYTOKEN_KEY = "authentication.gatewaytoken.key";
@Test
@Description("The 'multi.assignments.enabled' property must not be changed to false.")
@@ -48,6 +56,36 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg
.andExpect(status().isForbidden());
}
@Test
@Description("The Batch configuration should be applied")
public void changeBatchConfiguration() throws Exception {
JSONObject configuration = new JSONObject();
configuration.put(ROLLOUT_APPROVAL_ENABLED, true);
configuration.put(AUTHENTICATION_GATEWAYTOKEN_ENABLED, true);
configuration.put(AUTHENTICATION_GATEWAYTOKEN_KEY, "1234");
String body = configuration.toString();
mvc.perform(put(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs")
.content(body).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
.andExpect(status().isOk());
}
@Test
@Description("The Batch configuration should not be applied")
public void changeBatchConfigurationFail() throws Exception {
JSONObject configuration = new JSONObject();
configuration.put(ROLLOUT_APPROVAL_ENABLED, true);
configuration.put(AUTHENTICATION_GATEWAYTOKEN_ENABLED, "wrong");
configuration.put(AUTHENTICATION_GATEWAYTOKEN_KEY, "1234");
String body = configuration.toString();
mvc.perform(put(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs")
.content(body).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
.andExpect(status().isBadRequest());
}
@Test
@Description("The 'repository.actions.autoclose.enabled' property must not be modified if Multi-Assignments is enabled.")
public void autoCloseCannotBeModifiedIfMultiAssignmentIsEnabled() throws Exception {