Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch system config update #1402

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@
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;
Expand All @@ -26,6 +32,8 @@
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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -139,39 +153,68 @@ public <T> T getGlobalConfigurationValue(final String configurationKeyName, fina
ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY))
public <T extends Serializable> TenantConfigurationValue<T> addOrUpdateConfiguration(
denislavprinov marked this conversation as resolved.
Show resolved Hide resolved
final String configurationKeyName, final T value) {
return addOrUpdateConfiguration0(Collections.singletonMap(configurationKeyName, value)).values().iterator().next();
}

final TenantConfigurationKey configurationKey = tenantConfigurationProperties.fromKeyName(configurationKeyName);

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()));
}
@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);
}
});

configurationKey.validate(applicationContext, value);
return addOrUpdateConfiguration0(configurations);
}

JpaTenantConfiguration tenantConfiguration = tenantConfigurationRepository
.findByKey(configurationKey.getKeyName());
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);

if (tenantConfiguration == null) {
tenantConfiguration = new JpaTenantConfiguration(configurationKey.getKeyName(), value.toString());
} else {
tenantConfiguration.setValue(value.toString());
}
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()));
}

assertValueChangeIsAllowed(configurationKeyName, tenantConfiguration);
configurationKey.validate(applicationContext, value);

final JpaTenantConfiguration updatedTenantConfiguration = tenantConfigurationRepository
.save(tenantConfiguration);
JpaTenantConfiguration tenantConfiguration = tenantConfigurationRepository
.findByKey(configurationKey.getKeyName());

@SuppressWarnings("unchecked")
final Class<T> clazzT = (Class<T>) value.getClass();
if (tenantConfiguration == null) {
tenantConfiguration = new JpaTenantConfiguration(configurationKey.getKeyName(), value.toString());
} else {
tenantConfiguration.setValue(value.toString());
}

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();
assertValueChangeIsAllowed(configurationKeyName, tenantConfiguration);
configurationList.add(tenantConfiguration);
});

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();
}));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void storeTenantSpecificConfigurationAsString() {

@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";
Expand All @@ -89,6 +89,22 @@ public void updateTenantSpecifcConfiguration() {
.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() {
Expand Down Expand Up @@ -118,6 +134,26 @@ public void wrongTenantConfigurationValueTypeThrowsException() {
}
}

@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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ public Serializable getValue() {
}

/**
* 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +21,7 @@
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.
Expand Down Expand Up @@ -83,4 +86,20 @@ ResponseEntity<MgmtSystemTenantConfigurationValue> getTenantConfigurationValue(
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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -76,4 +78,22 @@ public ResponseEntity<MgmtSystemTenantConfigurationValue> updateTenantConfigurat
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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand All @@ -48,6 +56,36 @@ public void deactivateMultiAssignment() throws Exception {
.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 {
Expand Down