diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationManagement.java index 7dc73d971..11a7b2157 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationManagement.java @@ -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) TenantConfigurationValue 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) + Map> addOrUpdateConfiguration(Map configurations); + /** * Build the tenant configuration by the given key * diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java index 4c453bf6c..ee443f1de 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantConfigurationManagement.java @@ -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; @@ -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; @@ -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 T getGlobalConfigurationValue(final String configurationKeyName, fina ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public TenantConfigurationValue addOrUpdateConfiguration( 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 Map> addOrUpdateConfiguration(Map 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 Map> addOrUpdateConfiguration0(Map configurations) { + List 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 clazzT = (Class) value.getClass(); + if (tenantConfiguration == null) { + tenantConfiguration = new JpaTenantConfiguration(configurationKey.getKeyName(), value.toString()); + } else { + tenantConfiguration.setValue(value.toString()); + } - return TenantConfigurationValue. 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 jpaTenantConfigurations = tenantConfigurationRepository + .saveAll(configurationList); + + return jpaTenantConfigurations.stream().collect(Collectors.toMap( + JpaTenantConfiguration::getKey, + updatedTenantConfiguration -> { + + @SuppressWarnings("unchecked") + final Class clazzT = (Class) configurations.get(updatedTenantConfiguration.getKey()).getClass(); + return TenantConfigurationValue.builder().global(false) + .createdBy(updatedTenantConfiguration.getCreatedBy()) + .createdAt(updatedTenantConfiguration.getCreatedAt()) + .lastModifiedAt(updatedTenantConfiguration.getLastModifiedAt()) + .lastModifiedBy(updatedTenantConfiguration.getLastModifiedBy()) + .value(conversionService.convert(updatedTenantConfiguration.getValue(), clazzT)) + .build(); + })); } /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TenantConfigurationManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TenantConfigurationManagementTest.java index 24a2f7378..be3b67b24 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TenantConfigurationManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TenantConfigurationManagementTest.java @@ -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"; @@ -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 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 void wrongTenantConfigurationValueTypeThrowsException() { } } + @Test + @Description("Tests that the get configuration throws exception in case the value is the wrong type") + public void batchWrongTenantConfigurationValueTypeThrowsException() { + Map 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() { diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/system/MgmtSystemTenantConfigurationValueRequest.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/system/MgmtSystemTenantConfigurationValueRequest.java index 45e39cd70..5c160b0fb 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/system/MgmtSystemTenantConfigurationValueRequest.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/system/MgmtSystemTenantConfigurationValueRequest.java @@ -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; } - } diff --git a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTenantManagementRestApi.java b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTenantManagementRestApi.java index dbfc9de48..cf4be7d70 100644 --- a/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTenantManagementRestApi.java +++ b/hawkbit-rest/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/rest/api/MgmtTenantManagementRestApi.java @@ -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.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 @@ ResponseEntity getTenantConfigurationValue( ResponseEntity 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> updateTenantConfiguration( + @RequestBody Map configurationValueMap); + } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResource.java b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResource.java index d14cb66c7..cebc32fae 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResource.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResource.java @@ -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 ResponseEntity updateTenantConfigurat return ResponseEntity.ok(MgmtTenantManagementMapper.toResponse(keyName, updatedValue)); } + @Override + public ResponseEntity> updateTenantConfiguration( + Map configurationValueMap) { + + boolean containsNull = configurationValueMap.keySet().stream() + .anyMatch(Objects::isNull); + + if (containsNull) { + return ResponseEntity.badRequest().build(); + } + + Map> tenantConfigurationValues = tenantConfigurationManagement + .addOrUpdateConfiguration(configurationValueMap); + + return ResponseEntity.ok(tenantConfigurationValues.entrySet().stream().map(entry -> + MgmtTenantManagementMapper.toResponse(entry.getKey(), entry.getValue())).toList()); + } + } diff --git a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java index a9e4bd5c9..c1f430961 100644 --- a/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java +++ b/hawkbit-rest/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java @@ -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 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 {