diff --git a/hawkbit-ddi/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiPolling.java b/hawkbit-ddi/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiPolling.java index 0c57b340e..7877e2121 100644 --- a/hawkbit-ddi/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiPolling.java +++ b/hawkbit-ddi/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiPolling.java @@ -26,7 +26,7 @@ import lombok.Data; @Schema(description = "Suggested sleep time between polls") public class DdiPolling { - @Schema(description = "Sleep time in HH:MM:SS notation", pattern = "HH:MM:SS", example = "12:00:00") + @Schema(description = "Sleep time in HH:mm:ss notation", pattern = "HH:mm:ss", example = "12:00:00") private final String sleep; /** diff --git a/hawkbit-ddi/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java b/hawkbit-ddi/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java index f0fc680cb..b3c459cf9 100644 --- a/hawkbit-ddi/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java +++ b/hawkbit-ddi/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java @@ -155,10 +155,12 @@ public class DdiRootController implements DdiRootControllerRestApi { checkAndCancelExpiredAction(activeAction); // activeAction - return new ResponseEntity<>(DataConversionHelper.fromTarget(target, installedAction, activeAction, + return new ResponseEntity<>(DataConversionHelper.fromTarget( + target, + installedAction, activeAction, activeAction == null - ? controllerManagement.getPollingTime() - : controllerManagement.getPollingTimeForAction(activeAction), tenantAware), + ? controllerManagement.getPollingTime(target) + : controllerManagement.getPollingTimeForAction(target, activeAction), tenantAware), HttpStatus.OK); } diff --git a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java index fecf0af43..c410a269b 100644 --- a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java +++ b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java @@ -30,6 +30,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.util.Collections; import java.util.Map; import java.util.UUID; +import java.util.concurrent.Callable; import org.eclipse.hawkbit.ddi.json.model.DdiResult; import org.eclipse.hawkbit.ddi.json.model.DdiStatus; @@ -38,6 +39,7 @@ import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent; +import org.eclipse.hawkbit.repository.event.remote.TenantConfigurationDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent; @@ -103,7 +105,6 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(status().isOk()) .andExpect(content().contentType(MediaTypes.HAL_JSON)) .andReturn(); - // verify that we did not specify a content-type in the request, in case there are any default values assertThat(result.getRequest().getHeader("Accept")).isNull(); } @@ -124,7 +125,6 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { testdataFactory.createTarget(knownTargetControllerId); final Target findTargetByControllerID = targetManagement.getByControllerID(knownTargetControllerId).get(); assertThat(findTargetByControllerID.getCreatedBy()).isEqualTo(knownCreatedBy); - // make a poll, audit information should not be changed, run as controller principal! SecurityContextSwitch.runAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { @@ -133,7 +133,6 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(status().isOk()); return null; }); - // verify that audit information has not changed final Target targetVerify = targetManagement.getByControllerID(knownTargetControllerId).get(); assertThat(targetVerify.getCreatedBy()).isEqualTo(findTargetByControllerID.getCreatedBy()); @@ -161,7 +160,6 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) void rootRsPlugAndPlay() throws Exception { - final long current = System.currentTimeMillis(); final String controllerId = "4711"; @@ -172,7 +170,6 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))); assertThat(targetManagement.getByControllerID(controllerId).get().getLastTargetQuery()) .isGreaterThanOrEqualTo(current); - assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.REGISTERED); @@ -194,26 +191,46 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { * Ensures that tenant specific polling time, which is saved in the db, is delivered to the controller. */ @Test - @WithUser(principal = "knownpricipal", allSpPermissions = false) + @WithUser(principal = "knownpricipal") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1), - @Expect(type = TenantConfigurationCreatedEvent.class, count = 1) }) + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1), + @Expect(type = TenantConfigurationDeletedEvent.class, count = 1) }) void pollWithModifiedGlobalPollingTime() throws Exception { - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION), + withPollingTime("00:02:00", () -> SecurityContextSwitch.runAs( + SecurityContextSwitch.withUser("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { - tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME_INTERVAL, "00:02:00"); + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:02:00"))); return null; - }); + })); + } - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { - mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)) - .andDo(MockMvcResultPrinter.print()) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaTypes.HAL_JSON)) - .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:02:00"))); - return null; - }); + /** + * Ensures that tenant specific polling time, which is saved in the db, is delivered to the controller. + */ + @Test + @WithUser(principal = "knownpricipal") + @ExpectEvents({ + @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetPollEvent.class, count = 1), + @Expect(type = TenantConfigurationCreatedEvent.class, count = 1), + @Expect(type = TenantConfigurationDeletedEvent.class, count = 1) }) + void pollWithModifiedWithOverridesGlobalPollingTime() throws Exception { + withPollingTime("00:02:00, controllerid == 4711 -> 00:01:00", () -> SecurityContextSwitch.runAs( + SecurityContextSwitch.withUser("controller", CONTROLLER_ROLE_ANONYMOUS), + () -> { + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)) + .andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaTypes.HAL_JSON)) + .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))); + return null; + })); } /** @@ -242,21 +259,20 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))) .andReturn().getResponse() .getHeader("ETag"); - mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).header("If-None-Match", etag)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isNotModified()); final Target target = targetManagement.getByControllerID(controllerId).get(); final DistributionSet ds = testdataFactory.createDistributionSet(""); - assignDistributionSet(ds.getId(), controllerId); - final Action updateAction = deploymentManagement.findActiveActionsByTarget(target.getControllerId(), PAGE) - .getContent().get(0); + final Action updateAction = deploymentManagement.findActiveActionsByTarget(target.getControllerId(), PAGE).getContent().get(0); final String etagWithFirstUpdate = mvc .perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId) - .header("If-None-Match", etag).accept(MediaType.APPLICATION_JSON).with(new RequestOnHawkbitDefaultPortPostProcessor())) + .header("If-None-Match", etag) + .accept(MediaType.APPLICATION_JSON) + .with(new RequestOnHawkbitDefaultPortPostProcessor())) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) @@ -265,9 +281,7 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(jsonPath("$._links.deploymentBase.href", startsWith(deploymentBaseLink("4711", updateAction.getId().toString())))) .andReturn().getResponse().getHeader("ETag"); - assertThat(etagWithFirstUpdate).isNotNull(); - mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).header("If-None-Match", etagWithFirstUpdate).with(new RequestOnHawkbitDefaultPortPostProcessor())) .andDo(MockMvcResultPrinter.print()) @@ -277,7 +291,6 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { sendDeploymentActionFeedback(target, updateAction, "closed", null) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - // as the update was installed, and we always receive the installed action, the // original state cannot be restored final String etagAfterInstallation = mvc @@ -295,11 +308,8 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { // Now another deployment final DistributionSet ds2 = testdataFactory.createDistributionSet("2"); - assignDistributionSet(ds2.getId(), controllerId); - final Action updateAction2 = deploymentManagement.findActiveActionsByTarget(target.getControllerId(), PAGE).getContent().get(0); - mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId) .header("If-None-Match", etagAfterInstallation).accept(MediaType.APPLICATION_JSON) .with(new RequestOnHawkbitDefaultPortPostProcessor())) @@ -312,7 +322,6 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(jsonPath("$._links.deploymentBase.href", startsWith(deploymentBaseLink("4711", updateAction2.getId().toString())))) .andReturn().getResponse().getHeader("ETag"); - } /** @@ -322,26 +331,22 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) void rootRsPreCommissioned() throws Exception { final String controllerId = "4711"; testdataFactory.createTarget(controllerId); - assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus()).isEqualTo(TargetUpdateStatus.UNKNOWN); - final long current = System.currentTimeMillis(); mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()) .andExpect(content().contentType(MediaTypes.HAL_JSON)) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00"))); - assertThat(targetManagement.getByControllerID(controllerId).get().getLastTargetQuery()) .isLessThanOrEqualTo(System.currentTimeMillis()); assertThat(targetManagement.getByControllerID(controllerId).get().getLastTargetQuery()) .isGreaterThanOrEqualTo(current); - assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus()) .isEqualTo(TargetUpdateStatus.REGISTERED); } @@ -357,7 +362,6 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { // test final String knownControllerId1 = "0815"; final long create = System.currentTimeMillis(); - // make a poll, audit information should be set on plug and play SecurityContextSwitch.runAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { @@ -366,7 +370,6 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { .andExpect(status().isOk()); return null; }); - // verify final Target target = targetManagement.getByControllerID(knownControllerId1).get(); assertThat(target.getAddress()).isEqualTo(IpUtil.createHttpUri("127.0.0.1")); @@ -385,17 +388,14 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Expect(type = TargetPollEvent.class, count = 1) }) void rootRsIpAddressNotStoredIfDisabled() throws Exception { securityProperties.getClients().setTrackRemoteIp(false); - // test final String knownControllerId1 = "0815"; mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownControllerId1)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - // verify final Target target = targetManagement.getByControllerID(knownControllerId1).get(); assertThat(target.getAddress()).isEqualTo(IpUtil.createHttpUri("***")); - securityProperties.getClients().setTrackRemoteIp(true); } @@ -417,16 +417,13 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final DistributionSet ds = testdataFactory.createDistributionSet(""); Target savedTarget = testdataFactory.createTarget("911"); savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId())); - final Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget.getControllerId(), PAGE) - .getContent().get(0); + final Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget.getControllerId(), PAGE).getContent().get(0); sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "failure") .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success") .andDo(MockMvcResultPrinter.print()) .andExpect(status().isGone()); @@ -453,14 +450,11 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final Target savedTarget = testdataFactory.createTarget("922"); final Map attributes = Collections.singletonMap("AttributeKey", "AttributeValue"); assertThatAttributesUpdateIsRequested(savedTarget.getControllerId()); - mvc.perform(put(CONTROLLER_BASE + "/configData", tenantAware.getCurrentTenant(), savedTarget.getControllerId()) .content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); assertThatAttributesUpdateIsNotRequested(savedTarget.getControllerId()); - assertAttributesUpdateNotRequestedAfterFailedDeployment(savedTarget, ds); - assertAttributesUpdateRequestedAfterSuccessfulDeployment(savedTarget, ds); } @@ -483,31 +477,23 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final DistributionSet ds = testdataFactory.createDistributionSet(""); Target savedTarget = testdataFactory.createTarget("911"); savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId())); - final Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget.getControllerId(), PAGE) - .getContent().get(0); - + final Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget.getControllerId(), PAGE).getContent().get(0); sendDeploymentActionFeedback(savedTarget, savedAction, "scheduled", null, TARGET_SCHEDULED_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null, TARGET_PROCEEDING_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory=2", tenantAware.getCurrentTenant(), 911, savedAction.getId()) .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.actionHistory.messages", - hasItem(containsString(TARGET_COMPLETED_INSTALLATION_MSG)))) - .andExpect(jsonPath("$.actionHistory.messages", - hasItem(containsString(TARGET_PROCEEDING_INSTALLATION_MSG)))) - .andExpect(jsonPath("$.actionHistory.messages", - not(hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG))))); + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString(TARGET_COMPLETED_INSTALLATION_MSG)))) + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString(TARGET_PROCEEDING_INSTALLATION_MSG)))) + .andExpect(jsonPath("$.actionHistory.messages", not(hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG))))); } /** @@ -530,25 +516,20 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { Target savedTarget = testdataFactory.createTarget("911"); savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId())); final Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget.getControllerId(), PAGE).getContent().get(0); - sendDeploymentActionFeedback(savedTarget, savedAction, "scheduled", null, TARGET_SCHEDULED_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null, TARGET_PROCEEDING_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory=0", tenantAware.getCurrentTenant(), 911, savedAction.getId()) .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.actionHistory.messages").doesNotExist()); - mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory", tenantAware.getCurrentTenant(), 911, savedAction.getId()) .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()) @@ -567,7 +548,7 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock @Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), - @Expect(type = ActionCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 2), @Expect(type = TargetAttributesRequestedEvent.class, count = 1) }) @@ -575,31 +556,23 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final DistributionSet ds = testdataFactory.createDistributionSet(""); Target savedTarget = testdataFactory.createTarget("911"); savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId())); - final Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget.getControllerId(), PAGE) - .getContent().get(0); - + final Action savedAction = deploymentManagement.findActiveActionsByTarget(savedTarget.getControllerId(), PAGE).getContent().get(0); sendDeploymentActionFeedback(savedTarget, savedAction, "scheduled", null, TARGET_SCHEDULED_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, "proceeding", null, TARGET_PROCEEDING_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - sendDeploymentActionFeedback(savedTarget, savedAction, "closed", "success", TARGET_COMPLETED_INSTALLATION_MSG) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); - mvc.perform(get(DEPLOYMENT_BASE + "?actionHistory=-1", tenantAware.getCurrentTenant(), 911, savedAction.getId()) .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.actionHistory.messages", - hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG)))) - .andExpect(jsonPath("$.actionHistory.messages", - hasItem(containsString(TARGET_PROCEEDING_INSTALLATION_MSG)))) - .andExpect(jsonPath("$.actionHistory.messages", - hasItem(containsString(TARGET_COMPLETED_INSTALLATION_MSG)))); + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString(TARGET_SCHEDULED_INSTALLATION_MSG)))) + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString(TARGET_PROCEEDING_INSTALLATION_MSG)))) + .andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString(TARGET_COMPLETED_INSTALLATION_MSG)))); } /** @@ -611,14 +584,13 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION), () -> { - tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME_INTERVAL, - "00:05:00"); - tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.MIN_POLLING_TIME_INTERVAL, "00:01:00"); + tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME, "00:05:00"); return null; }); final Target savedTarget = testdataFactory.createTarget("1911"); - assignDistributionSetWithMaintenanceWindow(ds.getId(), savedTarget.getControllerId(), getTestSchedule(16), + assignDistributionSetWithMaintenanceWindow( + ds.getId(), savedTarget.getControllerId(), getTestSchedule(16), getTestDuration(10), getTestTimeZone()).getAssignedEntity().iterator().next(); mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")) @@ -627,9 +599,9 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final Target savedTarget1 = testdataFactory.createTarget("2911"); final DistributionSet ds1 = testdataFactory.createDistributionSet("1"); - assignDistributionSetWithMaintenanceWindow(ds1.getId(), savedTarget1.getControllerId(), getTestSchedule(10), + assignDistributionSetWithMaintenanceWindow( + ds1.getId(), savedTarget1.getControllerId(), getTestSchedule(10), getTestDuration(10), getTestTimeZone()).getAssignedEntity().iterator().next(); - mvc.perform(get(CONTROLLER_BASE, "default-tenant", "2911")) .andExpect(status().isOk()) .andExpect(jsonPath("$.config.polling.sleep", lessThan("00:05:00"))) @@ -637,7 +609,8 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final Target savedTarget2 = testdataFactory.createTarget("3911"); final DistributionSet ds2 = testdataFactory.createDistributionSet("2"); - assignDistributionSetWithMaintenanceWindow(ds2.getId(), savedTarget2.getControllerId(), getTestSchedule(5), + assignDistributionSetWithMaintenanceWindow( + ds2.getId(), savedTarget2.getControllerId(), getTestSchedule(5), getTestDuration(5), getTestTimeZone()).getAssignedEntity().iterator().next(); mvc.perform(get(CONTROLLER_BASE, "default-tenant", "3911")) @@ -646,9 +619,9 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final Target savedTarget3 = testdataFactory.createTarget("4911"); final DistributionSet ds3 = testdataFactory.createDistributionSet("3"); - assignDistributionSetWithMaintenanceWindow(ds3.getId(), savedTarget3.getControllerId(), getTestSchedule(-5), + assignDistributionSetWithMaintenanceWindow( + ds3.getId(), savedTarget3.getControllerId(), getTestSchedule(-5), getTestDuration(15), getTestTimeZone()).getAssignedEntity().iterator().next(); - mvc.perform(get(CONTROLLER_BASE, "default-tenant", "4911")) .andExpect(status().isOk()) .andExpect(jsonPath("$.config.polling.sleep", equalTo("00:05:00"))); @@ -661,16 +634,12 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { void downloadAndUpdateStatusBeforeMaintenanceWindowStartTime() throws Exception { Target savedTarget = testdataFactory.createTarget("1911"); final DistributionSet ds = testdataFactory.createDistributionSet(""); - savedTarget = getFirstAssignedTarget(assignDistributionSetWithMaintenanceWindow(ds.getId(), - savedTarget.getControllerId(), getTestSchedule(2), getTestDuration(1), getTestTimeZone())); - - mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")) - .andExpect(status().isOk()); + savedTarget = getFirstAssignedTarget(assignDistributionSetWithMaintenanceWindow( + ds.getId(), savedTarget.getControllerId(), getTestSchedule(2), getTestDuration(1), getTestTimeZone())); + mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk()); final Action action = deploymentManagement.findActiveActionsByTarget(savedTarget.getControllerId(), PAGE).getContent().get(0); - - mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "1911", action.getId()) - .accept(MediaType.APPLICATION_JSON)) + mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "1911", action.getId()).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.deployment.download", equalTo("forced"))) @@ -685,16 +654,13 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { void downloadAndUpdateStatusDuringMaintenanceWindow() throws Exception { Target savedTarget = testdataFactory.createTarget("1911"); final DistributionSet ds = testdataFactory.createDistributionSet(""); - savedTarget = getFirstAssignedTarget(assignDistributionSetWithMaintenanceWindow(ds.getId(), - savedTarget.getControllerId(), getTestSchedule(-5), getTestDuration(10), getTestTimeZone())); + savedTarget = getFirstAssignedTarget(assignDistributionSetWithMaintenanceWindow( + ds.getId(), savedTarget.getControllerId(), getTestSchedule(-5), getTestDuration(10), getTestTimeZone())); - mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")) - .andExpect(status().isOk()); + mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk()); final Action action = deploymentManagement.findActiveActionsByTarget(savedTarget.getControllerId(), PAGE).getContent().get(0); - - mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "1911", action.getId()) - .accept(MediaType.APPLICATION_JSON)) + mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "1911", action.getId()).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.deployment.download", equalTo("forced"))) @@ -725,42 +691,37 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Test void rootRsWithInvalidControllerId() throws Exception { final String invalidControllerId = randomString(Target.CONTROLLER_ID_MAX_SIZE + 1); - mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), invalidControllerId)) - .andExpect(status().isBadRequest()); + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), invalidControllerId)).andExpect(status().isBadRequest()); } private void assertAttributesUpdateNotRequestedAfterFailedDeployment(Target target, final DistributionSet ds) throws Exception { target = getFirstAssignedTarget(assignDistributionSet(ds.getId(), target.getControllerId())); final Action action = deploymentManagement.findActiveActionsByTarget(target.getControllerId(), PAGE).getContent().get(0); - sendDeploymentActionFeedback(target, action, "closed", "failure") - .andExpect(status().isOk()); + sendDeploymentActionFeedback(target, action, "closed", "failure").andExpect(status().isOk()); assertThatAttributesUpdateIsNotRequested(target.getControllerId()); } private void assertAttributesUpdateRequestedAfterSuccessfulDeployment(Target target, final DistributionSet ds) throws Exception { target = getFirstAssignedTarget(assignDistributionSet(ds.getId(), target.getControllerId())); final Action action = deploymentManagement.findActiveActionsByTarget(target.getControllerId(), PAGE).getContent().get(0); - sendDeploymentActionFeedback(target, action, "closed", null) - .andExpect(status().isOk()); + sendDeploymentActionFeedback(target, action, "closed", null).andExpect(status().isOk()); assertThatAttributesUpdateIsRequested(target.getControllerId()); } private void assertThatAttributesUpdateIsRequested(final String targetControllerId) throws Exception { - mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), targetControllerId) - .accept(MediaType.APPLICATION_JSON)) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), targetControllerId).accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$._links.configData.href").isNotEmpty()); } private void assertThatAttributesUpdateIsNotRequested(final String targetControllerId) throws Exception { - mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), targetControllerId) - .accept(MediaType.APPLICATION_JSON)) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), targetControllerId).accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$._links.configData").doesNotExist()); } - private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String execution, String finished, - String message) throws Exception { + private ResultActions sendDeploymentActionFeedback( + final Target target, final Action action, final String execution, String finished, String message) throws Exception { if (finished == null) { finished = "none"; } @@ -770,9 +731,8 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final String feedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.valueOf(execution.toUpperCase()), DdiResult.FinalResult.valueOf(finished.toUpperCase()), Collections.singletonList(message)); - return mvc.perform( - post(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), target.getControllerId(), action.getId()) - .content(feedback).contentType(MediaType.APPLICATION_JSON)); + return mvc.perform(post(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), target.getControllerId(), action.getId()) + .content(feedback).contentType(MediaType.APPLICATION_JSON)); } private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String execution, final String finished) @@ -781,12 +741,29 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { } private void assertDeploymentActionIsExposedToTarget(final String controllerId, final long expectedActionId) throws Exception { - final String expectedDeploymentBaseLink = String.format("/%s/controller/v1/%s/deploymentBase/%d", + final String expectedDeploymentBaseLink = String.format( + "/%s/controller/v1/%s/deploymentBase/%d", tenantAware.getCurrentTenant(), controllerId, expectedActionId); - mvc.perform( - get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON)) + mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()) .andExpect(jsonPath("$._links.deploymentBase.href", containsString(expectedDeploymentBaseLink))); } + + private void withPollingTime(final String pollingTime, final Callable runnable) throws Exception { + SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION), + () -> { + tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME, pollingTime); + return null; + }); + try { + runnable.call(); + } finally { + SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION), + () -> { + tenantConfigurationManagement.deleteConfiguration(TenantConfigurationKey.POLLING_TIME); + return null; + }); + } + } } \ No newline at end of file diff --git a/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/GatewayTokenAuthenticator.java b/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/GatewayTokenAuthenticator.java index f82608f67..d93d9206d 100644 --- a/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/GatewayTokenAuthenticator.java +++ b/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/GatewayTokenAuthenticator.java @@ -38,11 +38,11 @@ public class GatewayTokenAuthenticator extends Authenticator.AbstractAuthenticat super(tenantConfigurationManagement, tenantAware, systemSecurityContext); gatewaySecurityTokenKeyConfigRunner = () -> { log.trace("retrieving configuration value for configuration key {}", - TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY); + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY); return systemSecurityContext .runAsSystem(() -> tenantConfigurationManagement - .getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class) + .getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class) .getValue()); }; } @@ -78,6 +78,6 @@ public class GatewayTokenAuthenticator extends Authenticator.AbstractAuthenticat @Override protected String getTenantConfigurationKey() { - return TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED; + return TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED; } } \ No newline at end of file diff --git a/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/SecurityHeaderAuthenticator.java b/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/SecurityHeaderAuthenticator.java index cffece2e3..975a6c83e 100644 --- a/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/SecurityHeaderAuthenticator.java +++ b/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/SecurityHeaderAuthenticator.java @@ -58,7 +58,7 @@ public class SecurityHeaderAuthenticator extends Authenticator.AbstractAuthentic this.sslIssuerHashBasicHeader = caAuthorityNameHeader; sslIssuerNameConfigTenantRunner = () -> systemSecurityContext.runAsSystem( () -> tenantConfigurationManagement.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class).getValue()); + TenantConfigurationKey.AUTHENTICATION_HEADER_AUTHORITY_NAME, String.class).getValue()); } @Override @@ -95,7 +95,7 @@ public class SecurityHeaderAuthenticator extends Authenticator.AbstractAuthentic @Override protected String getTenantConfigurationKey() { - return TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED; + return TenantConfigurationKey.AUTHENTICATION_HEADER_ENABLED; } @Override diff --git a/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/SecurityTokenAuthenticator.java b/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/SecurityTokenAuthenticator.java index acec12423..4791ffd0d 100644 --- a/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/SecurityTokenAuthenticator.java +++ b/hawkbit-ddi/hawkbit-ddi-security/src/main/java/org/eclipse/hawkbit/security/controller/SecurityTokenAuthenticator.java @@ -77,6 +77,6 @@ public class SecurityTokenAuthenticator extends Authenticator.AbstractAuthentica @Override protected String getTenantConfigurationKey() { - return TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED; + return TenantConfigurationKey.AUTHENTICATION_TARGET_SECURITY_TOKEN_ENABLED; } } \ No newline at end of file diff --git a/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/GatewayTokenAuthenticatorTest.java b/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/GatewayTokenAuthenticatorTest.java index ce3141708..cc23cd637 100644 --- a/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/GatewayTokenAuthenticatorTest.java +++ b/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/GatewayTokenAuthenticatorTest.java @@ -67,10 +67,10 @@ class GatewayTokenAuthenticatorTest { void testWithGwToken() { final ControllerSecurityToken securityToken = prepareSecurityToken(GATEWAY_TOKEN); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class)) + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class)) .thenReturn(CONFIG_VALUE_GW_TOKEN); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_ENABLED); assertThat(authenticator.authenticate(securityToken)) @@ -85,10 +85,10 @@ class GatewayTokenAuthenticatorTest { void testWithBadGwToken() { final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_TOKEN); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class)) + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class)) .thenReturn(CONFIG_VALUE_GW_TOKEN); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_ENABLED); assertThat(authenticator.authenticate(securityToken)).isNull(); @@ -109,7 +109,7 @@ class GatewayTokenAuthenticatorTest { void testWithGwTokenButDisabled() { final ControllerSecurityToken securityToken = prepareSecurityToken(GATEWAY_TOKEN); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_DISABLED); assertThat(authenticator.authenticate(securityToken)).isNull(); diff --git a/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/SecurityHeaderAuthenticatorTest.java b/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/SecurityHeaderAuthenticatorTest.java index 02bb75292..20c89b0b1 100644 --- a/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/SecurityHeaderAuthenticatorTest.java +++ b/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/SecurityHeaderAuthenticatorTest.java @@ -78,10 +78,10 @@ class SecurityHeaderAuthenticatorTest { void testWithSingleKnownHash() { final ControllerSecurityToken securityToken = prepareSecurityToken(SINGLE_HASH); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class)) + TenantConfigurationKey.AUTHENTICATION_HEADER_AUTHORITY_NAME, String.class)) .thenReturn(CONFIG_VALUE_SINGLE_HASH); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_HEADER_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_ENABLED); assertThat(authenticator.authenticate(securityToken)) @@ -95,10 +95,10 @@ class SecurityHeaderAuthenticatorTest { @Test void testWithMultipleKnownHashes() { when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class)) + TenantConfigurationKey.AUTHENTICATION_HEADER_AUTHORITY_NAME, String.class)) .thenReturn(CONFIG_VALUE_MULTI_HASH); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_HEADER_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_ENABLED); assertThat(authenticator.authenticate(prepareSecurityToken(SINGLE_HASH))) @@ -119,10 +119,10 @@ class SecurityHeaderAuthenticatorTest { void testWithUnknownHash() { final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_HASH); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class)) + TenantConfigurationKey.AUTHENTICATION_HEADER_AUTHORITY_NAME, String.class)) .thenReturn(CONFIG_VALUE_MULTI_HASH); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_HEADER_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_ENABLED); assertThat(authenticator.authenticate(securityToken)).isNull(); @@ -155,7 +155,7 @@ class SecurityHeaderAuthenticatorTest { void testWithSingleKnownHashButDisabled() { final ControllerSecurityToken securityToken = prepareSecurityToken(SINGLE_HASH); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_HEADER_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_DISABLED); assertThat(authenticator.authenticate(securityToken)).isNull(); diff --git a/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/SecurityTokenAuthenticatorTest.java b/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/SecurityTokenAuthenticatorTest.java index 6e11191ea..f782f5963 100644 --- a/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/SecurityTokenAuthenticatorTest.java +++ b/hawkbit-ddi/hawkbit-ddi-security/src/test/java/org/eclipse/hawkbit/security/controller/SecurityTokenAuthenticatorTest.java @@ -72,7 +72,7 @@ class SecurityTokenAuthenticatorTest { void testWithSecToken() { final ControllerSecurityToken securityToken = prepareSecurityToken(SECURITY_TOKEN); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_ENABLED); final Target target = Mockito.mock(Target.class); @@ -92,7 +92,7 @@ class SecurityTokenAuthenticatorTest { void testWithBadSecToken() { final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_TOKEN); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_ENABLED); assertThat(authenticator.authenticate(securityToken)).isNull(); @@ -113,7 +113,7 @@ class SecurityTokenAuthenticatorTest { void testWithSecTokenButDisabled() { final ControllerSecurityToken securityToken = prepareSecurityToken(SECURITY_TOKEN); when(tenantConfigurationManagementMock.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class)) + TenantConfigurationKey.AUTHENTICATION_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class)) .thenReturn(CONFIG_VALUE_DISABLED); assertThat(authenticator.authenticate(securityToken)).isNull(); diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/system/MgmtSystemTenantConfigurationValue.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/system/MgmtSystemTenantConfigurationValue.java index 4b04ac7c0..09f9e2405 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/system/MgmtSystemTenantConfigurationValue.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/system/MgmtSystemTenantConfigurationValue.java @@ -32,8 +32,7 @@ import org.springframework.hateoas.RepresentationModel; @JsonIgnoreProperties(ignoreUnknown = true) @Schema(description = """ **properties**: - * **rollout.approval.enabled** - Boolean, The configuration key 'rollout.approval.enabled' defines if - approval mode for Rollout Management is enabled. + * **rollout.approval.enabled** - Boolean, The configuration key 'rollout.approval.enabled' defines if approval mode for Rollout Management is enabled. * **repository.actions.autoclose.enabled** - Boolean, The configuration key 'repository.actions.autoclose.enabled' defines if autoclose running actions with new Distribution Set assignment is enabled. * **user.confirmation.flow.enabled** - Boolean, The configuration key 'user.confirmation.flow.enabled' defines if confirmation is required when distribution set is assigned to target. * **authentication.gatewaytoken.enabled** - Boolean, The configuration key 'authentication.gatewaytoken.enabled' defines if the authentication mode 'gateway security token' is enabled. @@ -42,13 +41,12 @@ import org.springframework.hateoas.RepresentationModel; * **authentication.header.enabled** - Boolean, The configuration key 'authentication.header.enabled' defines if the authentication mode 'authority header' is enabled. * **maintenanceWindowPollCount** - Integer, The configuration key 'maintenanceWindowPollCount' defines the polling interval so that controller tries to poll at least these many times between the last polling and before start of maintenance window. The polling interval is bounded by configured pollingTime and minPollingTime. The polling interval is modified as per following scheme: pollingTime(@time=t) = (maintenanceWindowStartTime - t)/maintenanceWindowPollCount. * **authentication.targettoken.enabled** - Boolean, The configuration key 'authentication.targettoken.enabled' defines if the authentication mode 'target security token' is enabled. - * **pollingTime** - String, The configuration key 'pollingTime' defines the time interval between two poll requests of a target. - * **anonymous.download.enabled** - Boolean, The configuration key 'anonymous.download.enabled' defines if the anonymous download mode is enabled. - * **authentication.header.authority** - String, The configuration key 'authentication.header.authority' defines the name of the 'authority header'. * **minPollingTime** - String, The configuration key 'minPollingTime' defines the smallest time interval permitted between two poll requests of a target. + * **pollingTime** - String, The configuration key 'pollingTime' defines the time interval between two poll requests of a target. + * **pollingOverdueTime** - String, The configuration key 'pollingOverdueTime' defines the period of time after the SP server will recognize a target, which is not performing pull requests anymore. + * **authentication.header.authority** - String, The configuration key 'authentication.header.authority' defines the name of the 'authority header'. * **authentication.gatewaytoken.key** - String, The configuration key 'authentication.gatewaytoken.key' defines the key of the gateway security token. * **action.cleanup.actionStatus** - String, The configuration key 'action.cleanup.actionStatus' defines the list of action status that should be taken into account for the cleanup. - * **pollingOverdueTime** - String, The configuration key 'pollingOverdueTime' defines the period of time after the SP server will recognize a target, which is not performing pull requests anymore. * **multi.assignments.enabled** - Boolean, The configuration key 'multi.assignments.enabled' defines if multiple distribution sets can be assigned to the same targets. * **batch.assignments.enabled** - Boolean, The configuration key 'batch.assignments.enabled' defines if distribution set can be assigned to multiple targets in a single batch message. * **implicit.lock.enabled** - Boolean (true by default), The configuration key 'implicit.lock.enabled' defines if distribution set and their software modules shall be implicitly locked when assigned to target, rollout or target filter. diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java index 69ede9f0f..a3f10bac7 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java @@ -69,7 +69,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg void getTenantConfiguration() throws Exception { //Test TenantConfiguration property mvc.perform(get(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", - TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY)) + TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); } @@ -98,7 +98,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg final String json = mapper.writeValueAsString(bodyPut); mvc.perform(put(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", - TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY).content(json) + TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY).content(json) .contentType(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); @@ -282,7 +282,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg @Test void deleteTenantConfiguration() throws Exception { mvc.perform(delete(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", - TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY)) + TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY)) .andDo(MockMvcResultPrinter.print()) .andExpect(status().isOk()); } @@ -305,7 +305,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg void getTenantConfigurationReadGWToken() throws Exception { SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenant_admin", SpPermission.TENANT_CONFIGURATION), () -> { tenantConfigurationManagement.addOrUpdateConfiguration( - TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, + TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, "123"); return null; }); diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index 13b09c4cb..ce42c7436 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -35,6 +35,7 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -188,38 +189,24 @@ public interface ControllerManagement { /** * Returns configured polling interval at which the controller polls hawkBit server. * - * @return current {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}. + * @param target {@link Target} for which polling time is calculated (it could be overridden for a specific targets). + * @return current {@link TenantConfigurationKey#POLLING_TIME}. */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - String getPollingTime(); - - /** - * Returns the configured minimum polling interval. - * - * @return current {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}. - */ - @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - String getMinPollingTime(); - - /** - * Returns the count to be used for reducing polling interval while calling {@link ControllerManagement#getPollingTimeForAction(Action)}. - * - * @return configured value of {@link TenantConfigurationKey#MAINTENANCE_WINDOW_POLL_COUNT}. - */ - @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - int getMaintenanceWindowPollCount(); + String getPollingTime(Target target); /** * Returns polling time based on the maintenance window for an action. Server will reduce the polling interval as the start time for * maintenance window approaches, so that at least these many attempts are made between current polling until start of maintenance window. - * Poll time keeps reducing with MinPollingTime as lower limit {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}. After the start - * of maintenance window, it resets to default {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}. + * Poll time keeps reducing with MinPollingTime as lower limit {@link ControllerPollProperties#getMinPollingTime()}. After the start + * of maintenance window, it resets to default {@link TenantConfigurationKey#POLLING_TIME}. * + * @param target {@link Target} for which polling time is calculated * @param action {@link Action} for which polling time is calculated based on it having maintenance window or not - * @return current {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}. + * @return current {@link TenantConfigurationKey#POLLING_TIME}. */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - String getPollingTimeForAction(Action action); + String getPollingTimeForAction(Target target, Action action); /** * Checks if a given target has currently or has even been assigned to the given artifact through the action history list. This can e.g. 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 c3c9bd814..6f18a99fb 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 @@ -31,11 +31,10 @@ public interface TenantConfigurationManagement { * Adds or updates a specific configuration for a specific tenant. * * @param configurationKeyName the key of the configuration - * @param value the configuration value which will be written into the - * database. + * @param value the configuration value which will be written into the database. * @return the configuration value which was just 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 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) diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/ControllerPollProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/ControllerPollProperties.java index fc061c59c..26e57188a 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/ControllerPollProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/ControllerPollProperties.java @@ -16,11 +16,9 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** - * Defines global configuration for the controllers/clients on the provisioning - * targets/devices. - * - * Note: many of the controller related properties can be overridden on tenant - * level. + * Defines global configuration for the controllers/clients on the provisioning targets/devices. + *

+ * Note: many of the controller related properties can be overridden on tenant level. */ @Data @ConfigurationProperties(prefix = "hawkbit.controller") @@ -30,22 +28,24 @@ public class ControllerPollProperties implements Serializable { private static final long serialVersionUID = 1L; /** - * Maximum polling time that can be configured system-wide and by tenant in HH:MM:SS notation. + * Maximum polling time that can be configured system-wide and by tenant in HH:mm:ss notation. */ private String maxPollingTime = "23:59:59"; /** - * Minimum polling time that can be configured by a tenant in HH:MM:SS notation. + * Minimum polling time that can be configured by a tenant in HH:mm:ss notation. */ private String minPollingTime = "00:00:30"; /** - * Controller polling time that can be configured system-wide and by tenant in HH:MM:SS notation. + * Controller polling time that can be configured system-wide and by tenant in HH:mm:ss(~\d{1,2}%)? notation, plus + * followed (optionally and ordered) by a comma separated @lt;QL filter@gt; -@gt; polling time that overrides the + * default polling time for the targets that match the filter. */ private String pollingTime = "00:05:00"; /** - * Controller polling overdue time that can be configured system-wide and by tenant in HH:MM:SS notation. + * Controller polling overdue time that can be configured system-wide and by tenant in HH:mm:ss notation. */ private String pollingOverdueTime = "00:05:00"; diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java index 8805bfcb2..8f7a6af21 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java @@ -13,101 +13,78 @@ import java.time.Duration; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.time.temporal.TemporalAccessor; + +import lombok.NoArgsConstructor; /** - * This class is a helper for converting a duration into a string and for the - * other way. The string is in the format expected in configuration and database - * {@link #DURATION_FORMAT}. + * This class is a helper for converting a duration into a string and for the other way. The string is in the format expected + * in configuration and database - in {@link Duration} default format or in custom format like "01:00:00" or "01:01:50:50" + * (starting with seconds, minutes, hours, days from the end). */ +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public final class DurationHelper { - /** - * Format of the String expected in configuration file and in the database. - */ - public static final String DURATION_FORMAT = "HH:mm:ss"; - - private DurationHelper() { - // utility class - } - - /** - * Creates a {@link DurationRangeValidator}. - * - * @param min imum of range. - * @param max imum of range. - * @return {@link DurationRangeValidator} range. - */ - public static DurationRangeValidator durationRangeValidator(final Duration min, final Duration max) { - return new DurationRangeValidator(min, max); - } + private static final DateTimeFormatter DURATION_FORMATER = DateTimeFormatter.ofPattern("HH:mm:ss"); + private static final long SECONDS_PER_DAY = 24 * 60 * 60L; // 24 hours * 60 minutes * 60 seconds + private static final Duration DAY = Duration.ofDays(1); /** * Converts a Duration into a formatted String * * @param duration duration, which will be converted into a formatted String - * @return String in the duration format, specified at - * {@link #DURATION_FORMAT} + * @return String in the duration format, specified as HH:mm:ss or d+:HH:mm:ss */ - public static String durationToFormattedString(final Duration duration) { + public static String toString(final Duration duration) { if (duration == null) { return null; } - return LocalTime.ofNanoOfDay(duration.toNanos()).format(DateTimeFormatter.ofPattern(DURATION_FORMAT)); + if (duration.compareTo(DAY) < 0) { // backward compatible HH:mm:ss + return LocalTime.ofSecondOfDay(duration.toSeconds()).format(DURATION_FORMATER); + } else { // custom format d+:HH:mm:ss + return duration.toDays() + ":" + LocalTime.ofSecondOfDay(duration.toSeconds() % SECONDS_PER_DAY).format(DURATION_FORMATER); + } } /** * Converts a formatted String into a Duration object. * - * @param formattedDuration String in {@link #DURATION_FORMAT} - * @return duration + * @param durationStr String in {@link Duration} default format or in custom format like "01:00:00" or "01:01:50:50" + * (starting with seconds, minutes, hours, days from the end) + * @return duration as a {@link Duration} object * @throws DateTimeParseException when String is in wrong format */ - public static Duration formattedStringToDuration(final String formattedDuration) { - if (formattedDuration == null) { + public static Duration fromString(final String durationStr) { + if (durationStr == null) { return null; } - final TemporalAccessor ta = DateTimeFormatter.ofPattern(DURATION_FORMAT).parse(formattedDuration.trim()); - return Duration.between(LocalTime.MIDNIGHT, LocalTime.from(ta)); - } - - /** - * converts values of time constants to a Duration object.. - * - * @param hours count of hours - * @param minutes count of minutes - * @param seconds count of seconds - * @return duration - */ - public static Duration getDurationByTimeValues(final long hours, final long minutes, final long seconds) { - return Duration.ofHours(hours).plusMinutes(minutes).plusSeconds(seconds); - } - - /** - * Duration validation utility class. Checks if the requested duration is in - * the defined min/max range. - */ - public static final class DurationRangeValidator { - - private final Duration min; - private final Duration max; - - private DurationRangeValidator(final Duration min, final Duration max) { - this.min = min; - this.max = max; - } - - /** - * Checks if the requested duration is in the defined min/max range. - * - * @param duration to checked - * @return true if in time range - */ - public boolean isWithinRange(final Duration duration) { - return duration.compareTo(min) >= 0 && duration.compareTo(max) <= 0; + if (durationStr.charAt(0) == 'P') { + // Handle ISO-8601 format, e.g., "PT1H30M" + return Duration.parse(durationStr); + } else { + // Handle custom format, e.g., "01:00:00" or "01:01:50:50" + final String[] split = durationStr.split(":"); + if (split.length == 1) { // ss + return Duration.ofSeconds(Long.parseLong(split[0])); + } else if (split.length == 2) { // mm:ss + return Duration + .ofMinutes(Long.parseLong(split[0])) + .plusSeconds(Long.parseLong(split[1])); + } else if (split.length == 3) { // HH:mm:ss + return Duration + .ofHours(Long.parseLong(split[0])) + .plusMinutes(Long.parseLong(split[1])) + .plusSeconds(Long.parseLong(split[2])); + } else if (split.length == 4) { // d:HH:mm:ss + return Duration + .ofDays(Long.parseLong(split[0])) + .plusHours(Long.parseLong(split[1])) + .plusMinutes(Long.parseLong(split[2])) + .plusSeconds(Long.parseLong(split[3])); + } else { + throw new IllegalArgumentException("No more then 4 chunks (split by ':') are allowed in duration"); + } } } - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java new file mode 100644 index 000000000..892fdead4 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * 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.tenancy.configuration; + +import java.time.Duration; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.Value; +import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException; + +@Value +public class PollingTime { + + private static final Pattern OVERRIDE_PATTERN = Pattern.compile( + "\\s{0,5},\\s{0,5}(?[^,]*)\\s{0,5}->\\s{0,5}(?" + PollingInterval.POLLING_INTERVALE_REGEX + ")\\s{0,5}"); + + PollingInterval pollingInterval; + List overrides; + + public PollingTime(final String pollingTime) { + final int indexOfComma = pollingTime.indexOf(','); + if (indexOfComma == -1) { // no overrides + pollingInterval = new PollingInterval(pollingTime); + overrides = Collections.emptyList(); + } else { + // Extract the main polling interval and overrides + final String pollingIntervalStr = pollingTime.substring(0, indexOfComma); + pollingInterval = new PollingInterval(pollingIntervalStr); + overrides = new ArrayList<>(); + final String overridesStr = pollingTime.substring(indexOfComma).trim(); // with initial comma + final Matcher overridesMatcher = OVERRIDE_PATTERN.matcher(overridesStr); + for (int start = 0; start < overridesStr.length(); start = overridesMatcher.end()) { + if (overridesMatcher.find(start)) { + overrides.add(new Override( + overridesMatcher.group("qlStr").trim(), + new PollingInterval(overridesMatcher.group("pollInterval").trim()))); + } else { + throw new TenantConfigurationValidatorException("Invalid pollingTime overrides: " + overridesStr); + } + } + } + } + + @Value + public static class PollingInterval { + + private static final Random RANDOM = new Random(); + + public static final String POLLING_INTERVALE_REGEX = "\\s{0,5}(?\\d{2}:[0-5]\\d:[0-5]\\d)\\s{0,5}(~(?\\d{1,2})%)?\\s{0,5}"; + private static final Pattern POLLING_INTERVAL_PATTERN = Pattern.compile(POLLING_INTERVALE_REGEX); + + Duration interval; + int deviationPercent; + + public PollingInterval(final String pollingInterval) { + final Matcher matcher = POLLING_INTERVAL_PATTERN.matcher(pollingInterval); + if (matcher.matches()) { + try { + this.interval = DurationHelper.fromString(matcher.group("pollingInterval")); + } catch (final DateTimeParseException ex) { + throw new TenantConfigurationValidatorException( + "The given configuration value is expected as a string in the format HH:mm:ss(~\\d{1,2})?."); + } + this.deviationPercent = Optional.ofNullable(matcher.group("deviationPercent")).map(Integer::parseInt).orElse(0); + } else { + throw new TenantConfigurationValidatorException("Invalid pollingInterval: " + pollingInterval); + } + } + + public String getFormattedIntervalWithDeviation(final Duration minPollingTime, final Duration maxPollingTime) { + if (deviationPercent > 0) { + final long millis = interval.toMillis(); + final long maxDeviationMillis = (millis * deviationPercent) / 100; + final long deviation = RANDOM.nextLong(-maxDeviationMillis, maxDeviationMillis + 1); + if (deviation != 0) { + final Duration intervalWithDeviation = Duration.ofMillis(millis + deviation); + if (minPollingTime != null && intervalWithDeviation.compareTo(minPollingTime) < 0) { + return DurationHelper.toString(minPollingTime); + } else if (maxPollingTime != null && intervalWithDeviation.compareTo(maxPollingTime) > 0) { + return DurationHelper.toString(maxPollingTime); + } else { + return DurationHelper.toString(intervalWithDeviation); + } + } + } + + return DurationHelper.toString(interval); + } + } + + // This record holds the override information for a specific QL string and its associated polling interval. + public record Override(String qlStr, PollingInterval pollingInterval) {} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java index 7c0677210..6bc111d17 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/TenantConfigurationProperties.java @@ -67,50 +67,41 @@ public class TenantConfigurationProperties { /** * Header based authentication enabled. */ - public static final String AUTHENTICATION_MODE_HEADER_ENABLED = "authentication.header.enabled"; + public static final String AUTHENTICATION_HEADER_ENABLED = "authentication.header.enabled"; /** * Header based authentication authority name. */ - public static final String AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME = "authentication.header.authority"; + public static final String AUTHENTICATION_HEADER_AUTHORITY_NAME = "authentication.header.authority"; /** * Target token based authentication enabled. */ - public static final String AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED = "authentication.targettoken.enabled"; + public static final String AUTHENTICATION_TARGET_SECURITY_TOKEN_ENABLED = "authentication.targettoken.enabled"; /** * Gateway token based authentication enabled. */ - public static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED = "authentication.gatewaytoken.enabled"; + public static final String AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED = "authentication.gatewaytoken.enabled"; /** * Gateway token value. */ - public static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY = "authentication.gatewaytoken.key"; + public static final String AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY = "authentication.gatewaytoken.key"; /** - * See system default in - * {@link ControllerPollProperties#getPollingTime()}. + * See system default in {@link ControllerPollProperties#getPollingTime()}. */ - public static final String POLLING_TIME_INTERVAL = "pollingTime"; + public static final String POLLING_TIME = "pollingTime"; /** - * See system default in - * {@link ControllerPollProperties#getMinPollingTime()}. + * See system default in {@link ControllerPollProperties#getPollingOverdueTime()}. */ - public static final String MIN_POLLING_TIME_INTERVAL = "minPollingTime"; + public static final String POLLING_OVERDUE_TIME = "pollingOverdueTime"; /** - * See system default in - * {@link ControllerPollProperties#getMaintenanceWindowPollCount()}. + * See system default in {@link ControllerPollProperties#getMaintenanceWindowPollCount()}. */ public static final String MAINTENANCE_WINDOW_POLL_COUNT = "maintenanceWindowPollCount"; - /** - * See system default in - * {@link ControllerPollProperties#getPollingOverdueTime()}. - */ - public static final String POLLING_OVERDUE_TIME_INTERVAL = "pollingOverdueTime"; /** * Represents setting if approval for a rollout is needed. */ public static final String ROLLOUT_APPROVAL_ENABLED = "rollout.approval.enabled"; /** - * Repository on autoclose mode instead of canceling in case of new DS - * assignment over active actions. + * Repository on autoclose mode instead of canceling in case of new Distribution Set assignment over active actions. */ public static final String REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED = "repository.actions.autoclose.enabled"; /** @@ -118,7 +109,7 @@ public class TenantConfigurationProperties { */ public static final String ACTION_CLEANUP_ENABLED = "action.cleanup.enabled"; /** - * Specifies the action expiry in milli-seconds. + * Specifies the action expiry in milliseconds. */ public static final String ACTION_CLEANUP_ACTION_EXPIRY = "action.cleanup.actionExpiry"; /** @@ -156,11 +147,11 @@ public class TenantConfigurationProperties { /** * Validates if an object matches the allowed data format of the corresponding key * - * @param context application context * @param value which will be validated + * @param context application context * @throws TenantConfigurationValidatorException is thrown, when object is invalid */ - public void validate(final ApplicationContext context, final Object value) { + public void validate(final Object value, final ApplicationContext context) { if (validator == null) { Objects.requireNonNull(DEFAULT_TYPE_VALIDATORS.get(dataType), "No validator defined for " + keyName).validate(value); } else { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationDurationValidator.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationDurationValidator.java new file mode 100644 index 000000000..600768363 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationDurationValidator.java @@ -0,0 +1,35 @@ +/** + * 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.tenancy.configuration.validator; + +import static org.eclipse.hawkbit.tenancy.configuration.DurationHelper.fromString; + +import java.time.Duration; + +import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException; + +/** + * This class is used to validate, that the property is a String and that it is in the correct duration format. + */ +public class TenantConfigurationDurationValidator extends TenantConfigurationStringValidator { + + // Exception squid:S1166 - Hide origin exception + @SuppressWarnings({ "squid:S1166" }) + @Override + public void validate(final Object tenantConfigurationObject) { + super.validate(tenantConfigurationObject); + + final String tenantConfigurationString = (String) tenantConfigurationObject; + final Duration duration = fromString(tenantConfigurationString); + if (duration.isNegative()) { + throw new TenantConfigurationValidatorException("The given configuration value is not in the allowed to be negative."); + } + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationPollingDurationValidator.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationPollingDurationValidator.java deleted file mode 100644 index 9697073eb..000000000 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationPollingDurationValidator.java +++ /dev/null @@ -1,67 +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.tenancy.configuration.validator; - -import java.time.Duration; -import java.time.format.DateTimeParseException; - -import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException; -import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties; -import org.eclipse.hawkbit.tenancy.configuration.DurationHelper; - -/** - * This class is used to validate, that the property is a String and that it is in the correct duration format. - */ -public class TenantConfigurationPollingDurationValidator implements TenantConfigurationValidator { - - private final Duration minDuration; - private final Duration maxDuration; - - /** - * This constructor is called by {@link org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties} using - * ApplicationContext.getAutowireCapableBeanFactory().createBean(Class) to validate the polling duration configuration. - * This insures the wiring of the properties is done correctly. - * - * @param properties property accessor for poll configuration - */ - public TenantConfigurationPollingDurationValidator(final ControllerPollProperties properties) { - minDuration = DurationHelper.formattedStringToDuration(properties.getMinPollingTime()); - maxDuration = DurationHelper.formattedStringToDuration(properties.getMaxPollingTime()); - } - - @Override - // Exception squid:S1166 - Hide origin exception - @SuppressWarnings({ "squid:S1166" }) - public void validate(final Object tenantConfigurationObject) { - TenantConfigurationValidator.super.validate(tenantConfigurationObject); - final String tenantConfigurationString = (String) tenantConfigurationObject; - - final Duration tenantConfigurationValue; - try { - tenantConfigurationValue = DurationHelper.formattedStringToDuration(tenantConfigurationString); - } catch (final DateTimeParseException ex) { - throw new TenantConfigurationValidatorException( - String.format("The given configuration value is expected as a string in the format %s.", - DurationHelper.DURATION_FORMAT)); - } - - if (!DurationHelper.durationRangeValidator(minDuration, maxDuration).isWithinRange(tenantConfigurationValue)) { - throw new TenantConfigurationValidatorException( - String.format("The given configuration value is not in the allowed range from %s to %s.", - DurationHelper.durationToFormattedString(minDuration), - DurationHelper.durationToFormattedString(maxDuration))); - } - } - - @Override - public Class validateToClass() { - return String.class; - } -} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationPollingTimeValidator.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationPollingTimeValidator.java new file mode 100644 index 000000000..fec84bee7 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationPollingTimeValidator.java @@ -0,0 +1,67 @@ +/** + * 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.tenancy.configuration.validator; + +import static org.eclipse.hawkbit.tenancy.configuration.DurationHelper.fromString; + +import java.time.Duration; + +import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException; +import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties; +import org.eclipse.hawkbit.tenancy.configuration.DurationHelper; +import org.eclipse.hawkbit.tenancy.configuration.PollingTime; + +/** + * This class is used to validate, that the property is a String and that it is in the correct polling time format. + */ +public class TenantConfigurationPollingTimeValidator extends TenantConfigurationStringValidator { + + private final Duration minPollingInterval; + private final Duration maxPollingInterval; + + /** + * This constructor is called by {@link org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties} using + * ApplicationContext.getAutowireCapableBeanFactory().createBean(Class) to validate the polling duration configuration. + * This insures the wiring of the properties is done correctly. + * + * @param properties property accessor for poll configuration + */ + public TenantConfigurationPollingTimeValidator(final ControllerPollProperties properties) { + this.minPollingInterval = fromString(properties.getMinPollingTime()); + this.maxPollingInterval = fromString(properties.getMaxPollingTime()); + } + + @Override + public void validate(final Object tenantConfigurationObject) { + super.validate(tenantConfigurationObject); + final String tenantConfigurationString = (String) tenantConfigurationObject; + + // validate parsable + final PollingTime pollingTime = new PollingTime(tenantConfigurationString); + // validate polling interval in range + validateInRange(pollingTime.getPollingInterval().getInterval()); + for (final PollingTime.Override override : pollingTime.getOverrides()) { + validateInRange(override.pollingInterval().getInterval()); + } + } + + private void validateInRange(final Duration pollingInterval) { + if (pollingInterval.compareTo(minPollingInterval) < 0) { + throw new TenantConfigurationValidatorException(String.format( + "The polling interval is smaller then minimum polling interval. The allowed range is [%s, %s].", + DurationHelper.toString(minPollingInterval), DurationHelper.toString(maxPollingInterval))); + } + if (pollingInterval.compareTo(maxPollingInterval) > 0) { + throw new TenantConfigurationValidatorException(String.format( + "The polling interval is bigger then minimum polling interval. The allowed range is [%s, %s].", + DurationHelper.toString(minPollingInterval), DurationHelper.toString(maxPollingInterval))); + } + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationValidator.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationValidator.java index 80815c012..9b9ce55cd 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationValidator.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/validator/TenantConfigurationValidator.java @@ -12,12 +12,12 @@ package org.eclipse.hawkbit.tenancy.configuration.validator; import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException; /** - * base interface for clases which can validate tenant configuration values. + * base interface for classes which can validate tenant configuration values. */ public interface TenantConfigurationValidator { /** - * validates the tenant configuration value + * Validates the tenant configuration value * * @param tenantConfigurationValue value which will be validated. * @throws TenantConfigurationValidatorException is thrown, when parameter is invalid. diff --git a/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/PollingTimeTest.java b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/PollingTimeTest.java new file mode 100644 index 000000000..d741499b3 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/test/java/org/eclipse/hawkbit/repository/PollingTimeTest.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * 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.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.eclipse.hawkbit.tenancy.configuration.DurationHelper; +import org.eclipse.hawkbit.tenancy.configuration.PollingTime; +import org.junit.jupiter.api.Test; + +class PollingTimeTest { + + @Test + void testBackwardsCompatibility() { + final PollingTime pollingTime = new PollingTime("01:00:00"); + assertThat(pollingTime.getPollingInterval().getInterval()).hasToString("PT1H"); + assertThat(pollingTime.getPollingInterval().getDeviationPercent()).isZero(); + assertThat(pollingTime.getOverrides()).isEmpty(); + } + + @Test + void testDeviation() { + final PollingTime pollingTime = new PollingTime("01:00:00~10%"); + final long maxDeviation = (Duration.ofHours(1).toMillis() / 10); + final long deviation = Duration.ofHours(1).toMillis() + - DurationHelper.fromString(pollingTime.getPollingInterval().getFormattedIntervalWithDeviation(null, null)).toMillis(); + assertThat(deviation) + .isGreaterThanOrEqualTo(-maxDeviation) + .isLessThanOrEqualTo(maxDeviation); + } + + @Test + void testComplexWithOverrides() { + assertExpectedComplexWithOverrides("01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync -> 00:05:00"); + } + + @Test + void testComplexWithOverridesWithWhitespaces() { + assertExpectedComplexWithOverrides("01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync ->00:05:00"); + assertExpectedComplexWithOverrides(" 01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync ->00:05:00 "); + assertExpectedComplexWithOverrides(" 01:00:00~10% , group == 'eu' -> 00:02:00 ~15%, status != in_sync ->00:05:00 "); + } + + private static void assertExpectedComplexWithOverrides(final String pollingTimeStr) { + final PollingTime pollingTime = new PollingTime(pollingTimeStr); + assertThat(pollingTime.getPollingInterval().getInterval()).hasToString("PT1H"); + assertThat(pollingTime.getPollingInterval().getDeviationPercent()).isEqualTo(10); + assertThat(pollingTime.getOverrides().get(0).qlStr()).isEqualTo("group == 'eu'"); + assertThat(pollingTime.getOverrides().get(0).pollingInterval().getInterval()).hasToString("PT2M"); + assertThat(pollingTime.getOverrides().get(0).pollingInterval().getDeviationPercent()).isEqualTo(15); + assertThat(pollingTime.getOverrides().get(1).qlStr()).isEqualTo("status != in_sync"); + assertThat(pollingTime.getOverrides().get(1).pollingInterval().getInterval()).hasToString("PT5M"); + assertThat(pollingTime.getOverrides().get(1).pollingInterval().getDeviationPercent()).isZero(); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/TimestampCalculator.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/TimestampCalculator.java index d2be5af86..3184d4ec1 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/TimestampCalculator.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/TimestampCalculator.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2015 Bosch Software Innovations GmbH and others + * Copyright (c) 2025 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -17,6 +17,8 @@ import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.configuration.DurationHelper; +import org.eclipse.hawkbit.tenancy.configuration.PollingTime; +import org.eclipse.hawkbit.tenancy.configuration.PollingTime.PollingInterval; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; /** @@ -29,19 +31,26 @@ public final class TimestampCalculator { /** * Calculates the overdue timestamp (overdue_ts) based on the current timestamp and the intervals for polling and poll-overdue: - *

+ *

* overdue_ts = now_ts - pollingInterval - pollingOverdueInterval;
* pollingInterval and pollingOverdueInterval are retrieved from tenant-specific system configuration. + *

+ * Note: this method checks against the default polling time interval. I.e. overrides are not considered. * * @return overdue_ts in milliseconds since Unix epoch as long value */ public static long calculateOverdueTimestamp() { - return System.currentTimeMillis() - getDurationForKey(TenantConfigurationKey.POLLING_TIME_INTERVAL).toMillis() - - getDurationForKey(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL).toMillis(); + return calculateOverdueTimestamp( + new PollingTime(getRawStringForKey(TenantConfigurationKey.POLLING_TIME)).getPollingInterval(), + DurationHelper.fromString(getRawStringForKey(TenantConfigurationKey.POLLING_OVERDUE_TIME))); } - private static Duration getDurationForKey(final String key) { - return DurationHelper.formattedStringToDuration(getRawStringForKey(key)); + private static long calculateOverdueTimestamp(final PollingInterval pollingInterval, final Duration pollingOverdueTime) { + return System.currentTimeMillis() + - (pollingInterval.getDeviationPercent() == 0 + ? pollingInterval.getInterval().toMillis() + : pollingInterval.getInterval().toMillis() * (100 + pollingInterval.getDeviationPercent()) / 100) + - pollingOverdueTime.toMillis(); } private static String getRawStringForKey(final String key) { diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties index 0b8e1e8b1..23974ba62 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties +++ b/hawkbit-repository/hawkbit-repository-core/src/main/resources/hawkbit-repository-defaults.properties @@ -8,7 +8,7 @@ # SPDX-License-Identifier: EPL-2.0 # -# Defines the polling time for the controllers in HH:MM:SS notation +# Defines the polling time for the controllers in HH:mm:ss notation hawkbit.controller.pollingTime=00:05:00 hawkbit.controller.pollingOverdueTime=00:05:00 hawkbit.controller.maxPollingTime=23:59:59 @@ -52,24 +52,16 @@ hawkbit.server.tenant.configuration.authentication-gatewaytoken-key.defaultValue hawkbit.server.tenant.configuration.polling-time.keyName=pollingTime hawkbit.server.tenant.configuration.polling-time.defaultValue=${hawkbit.controller.pollingTime} -hawkbit.server.tenant.configuration.polling-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationPollingDurationValidator - -hawkbit.server.tenant.configuration.min-polling-time.keyName=minPollingTime -hawkbit.server.tenant.configuration.min-polling-time.defaultValue=${hawkbit.controller.minPollingTime} -hawkbit.server.tenant.configuration.min-polling-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationPollingDurationValidator +hawkbit.server.tenant.configuration.polling-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationPollingTimeValidator hawkbit.server.tenant.configuration.polling-overdue-time.keyName=pollingOverdueTime hawkbit.server.tenant.configuration.polling-overdue-time.defaultValue=${hawkbit.controller.pollingOverdueTime} -hawkbit.server.tenant.configuration.polling-overdue-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationPollingDurationValidator +hawkbit.server.tenant.configuration.polling-overdue-time.validator=org.eclipse.hawkbit.tenancy.configuration.validator.TenantConfigurationDurationValidator hawkbit.server.tenant.configuration.maintenance-window-poll-count.keyName=maintenanceWindowPollCount hawkbit.server.tenant.configuration.maintenance-window-poll-count.defaultValue=${hawkbit.controller.maintenanceWindowPollCount} hawkbit.server.tenant.configuration.maintenance-window-poll-count.dataType=java.lang.Integer -#hawkbit.server.tenant.configuration.anonymous-download-enabled.keyName=anonymous.download.enabled -#hawkbit.server.tenant.configuration.anonymous-download-enabled.defaultValue=${hawkbit.server.download.anonymous.enabled} -#hawkbit.server.tenant.configuration.anonymous-download-enabled.dataType=java.lang.Boolean - hawkbit.server.tenant.configuration.rollout-approval-enabled.keyName=rollout.approval.enabled hawkbit.server.tenant.configuration.rollout-approval-enabled.defaultValue=false hawkbit.server.tenant.configuration.rollout-approval-enabled.dataType=java.lang.Boolean diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index edabfec66..064edad31 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -161,6 +161,7 @@ import org.eclipse.hawkbit.security.SecurityTokenGenerator; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver; +import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; import org.eclipse.hawkbit.utils.TenantConfigHelper; import org.springframework.beans.BeansException; @@ -850,16 +851,15 @@ public class RepositoryApplicationConfiguration { final DeploymentManagement deploymentManagement, final ConfirmationManagement confirmationManagement, final SoftwareModuleRepository softwareModuleRepository, final SoftwareModuleMetadataRepository softwareModuleMetadataRepository, final DistributionSetManagement distributionSetManagement, - final TenantConfigurationManagement tenantConfigurationManagement, + final TenantConfigurationManagement tenantConfigurationManagement, final ControllerPollProperties controllerPollProperties, final PlatformTransactionManager txManager, final EntityFactory entityFactory, final EntityManager entityManager, final AfterTransactionCommitExecutor afterCommit, final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, final ScheduledExecutorService executorService) { return new JpaControllerManagement(actionRepository, actionStatusRepository, quotaManagement, repositoryProperties, targetRepository, targetTypeManagement, deploymentManagement, confirmationManagement, softwareModuleRepository, - softwareModuleMetadataRepository, distributionSetManagement, tenantConfigurationManagement, txManager, - entityFactory, entityManager, afterCommit, systemSecurityContext, tenantAware, - executorService); + softwareModuleMetadataRepository, distributionSetManagement, tenantConfigurationManagement, controllerPollProperties, + txManager,entityFactory, entityManager, afterCommit, systemSecurityContext, tenantAware, executorService); } @Bean diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaControllerManagement.java index 3e891db3f..517a1c1e0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaControllerManagement.java @@ -79,6 +79,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaAction_; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; +import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher; import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository; import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository; import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleMetadataRepository; @@ -101,6 +102,9 @@ import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; +import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties; +import org.eclipse.hawkbit.tenancy.configuration.DurationHelper; +import org.eclipse.hawkbit.tenancy.configuration.PollingTime; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.springframework.dao.ConcurrencyFailureException; import org.springframework.data.domain.Page; @@ -116,6 +120,7 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallback; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; @@ -140,6 +145,8 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont private final SoftwareModuleMetadataRepository softwareModuleMetadataRepository; private final DistributionSetManagement distributionSetManagement; private final TenantConfigurationManagement tenantConfigurationManagement; + private final ControllerPollProperties controllerPollProperties; + private final Duration minPollingTime, maxPollingTime; private final PlatformTransactionManager txManager; private final EntityFactory entityFactory; private final EntityManager entityManager; @@ -155,7 +162,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont final DeploymentManagement deploymentManagement, final ConfirmationManagement confirmationManagement, final SoftwareModuleRepository softwareModuleRepository, final SoftwareModuleMetadataRepository softwareModuleMetadataRepository, final DistributionSetManagement distributionSetManagement, - final TenantConfigurationManagement tenantConfigurationManagement, + final TenantConfigurationManagement tenantConfigurationManagement, final ControllerPollProperties controllerPollProperties, final PlatformTransactionManager txManager, final EntityFactory entityFactory, final EntityManager entityManager, final AfterTransactionCommitExecutor afterCommit, final SystemSecurityContext systemSecurityContext, final TenantAware tenantAware, @@ -170,6 +177,13 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont this.softwareModuleMetadataRepository = softwareModuleMetadataRepository; this.distributionSetManagement = distributionSetManagement; this.tenantConfigurationManagement = tenantConfigurationManagement; + this.controllerPollProperties = controllerPollProperties; + minPollingTime = controllerPollProperties.getMinPollingTime() == null + ? Duration.of(0, ChronoUnit.SECONDS) + : DurationHelper.fromString(controllerPollProperties.getMinPollingTime()); + maxPollingTime = controllerPollProperties.getMaxPollingTime() == null + ? Duration.of(100, ChronoUnit.YEARS) + : DurationHelper.fromString(controllerPollProperties.getMaxPollingTime()); this.txManager = txManager; this.entityFactory = entityFactory; this.entityManager = entityManager; @@ -345,6 +359,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont public Target findOrRegisterTargetIfItDoesNotExist(final String controllerId, final URI address, final String name, final String type) { return findOrRegisterTargetIfItDoesNotExist0(controllerId, address, name, type); } + private Target findOrRegisterTargetIfItDoesNotExist0(final String controllerId, final URI address, final String name, final String type) { final Specification spec = (targetRoot, query, cb) -> cb.equal(targetRoot.get(JpaTarget_.controllerId), controllerId); return targetRepository.findOne(spec) @@ -373,42 +388,39 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont } @Override - public String getPollingTime() { - return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement - .getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class).getValue()); - } - - /** - * Returns the configured minimum polling interval. - * - * @return current {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}. - */ - @Override - public String getMinPollingTime() { - return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement - .getConfigurationValue(TenantConfigurationKey.MIN_POLLING_TIME_INTERVAL, String.class).getValue()); - } - - /** - * Returns the count to be used for reducing polling interval while calling {@link ControllerManagement#getPollingTimeForAction(Action)}. - * - * @return configured value of - * {@link TenantConfigurationKey#MAINTENANCE_WINDOW_POLL_COUNT}. - */ - @Override - public int getMaintenanceWindowPollCount() { - return systemSecurityContext.runAsSystem(() -> tenantConfigurationManagement - .getConfigurationValue(TenantConfigurationKey.MAINTENANCE_WINDOW_POLL_COUNT, Integer.class).getValue()); + public String getPollingTime(final Target target) { + return systemSecurityContext.runAsSystem(() -> { + final PollingTime pollingTime = new PollingTime( + tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.POLLING_TIME, String.class).getValue()); + if (!ObjectUtils.isEmpty(pollingTime.getOverrides()) && target instanceof JpaTarget jpaTarget) { + for (final PollingTime.Override override : pollingTime.getOverrides()) { + try { + if (EntityMatcher.forRsql(override.qlStr()).match(jpaTarget)) { + return override.pollingInterval().getFormattedIntervalWithDeviation(minPollingTime, maxPollingTime); + } + } catch (final Exception e) { + log.warn("Error while evaluating polling override for target {}: {}", jpaTarget.getId(), e.getMessage()); + } + } + } + // returns default - no overrides or not applicable for the target + return pollingTime.getPollingInterval().getFormattedIntervalWithDeviation(minPollingTime, maxPollingTime); + }); } @Override - public String getPollingTimeForAction(final Action action) { + public String getPollingTimeForAction(final Target target, final Action action) { + final String pollingTime = getPollingTime(target); if (!action.hasMaintenanceSchedule() || action.isMaintenanceScheduleLapsed()) { - return getPollingTime(); + return pollingTime; + } else { + // the count to be used for reducing polling interval -> the configured value of {@link TenantConfigurationKey#MAINTENANCE_WINDOW_POLL_COUNT} + final int maintenanceWindowPollCount = systemSecurityContext.runAsSystem( + () -> tenantConfigurationManagement.getConfigurationValue( + TenantConfigurationKey.MAINTENANCE_WINDOW_POLL_COUNT, Integer.class).getValue()); + return new EventTimer(pollingTime, controllerPollProperties.getMinPollingTime(), ChronoUnit.SECONDS) + .timeToNextEvent(maintenanceWindowPollCount, action.getMaintenanceWindowStartTime().orElse(null)); } - - return new EventTimer(getPollingTime(), getMinPollingTime(), ChronoUnit.SECONDS) - .timeToNextEvent(getMaintenanceWindowPollCount(), action.getMaintenanceWindowStartTime().orElse(null)); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTenantConfigurationManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTenantConfigurationManagement.java index 875880391..a712d298c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTenantConfigurationManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTenantConfigurationManagement.java @@ -9,8 +9,10 @@ */ package org.eclipse.hawkbit.repository.jpa.management; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY; import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.BATCH_ASSIGNMENTS_ENABLED; import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.MULTI_ASSIGNMENTS_ENABLED; +import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.POLLING_TIME; import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey.REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED; import java.io.Serializable; @@ -18,8 +20,8 @@ import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -33,7 +35,9 @@ import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorExce 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.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTenantConfiguration; +import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher; import org.eclipse.hawkbit.repository.jpa.repository.TenantConfigurationRepository; import org.eclipse.hawkbit.repository.model.PollStatus; import org.eclipse.hawkbit.repository.model.Target; @@ -42,6 +46,7 @@ import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.configuration.DurationHelper; +import org.eclipse.hawkbit.tenancy.configuration.PollingTime; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey; import org.springframework.cache.Cache; @@ -55,6 +60,7 @@ import org.springframework.dao.ConcurrencyFailureException; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ObjectUtils; import org.springframework.validation.annotation.Validated; /** @@ -90,9 +96,8 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana @Transactional @Retryable(retryFor = { 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(); + public TenantConfigurationValue addOrUpdateConfiguration(final String configurationKeyName, final T value) { + return addOrUpdateConfiguration0(Map.of(configurationKeyName, value)).values().iterator().next(); } @Override @@ -138,11 +143,9 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana checkAccess(configurationKeyName); final TenantConfigurationKey configurationKey = tenantConfigurationProperties.fromKeyName(configurationKeyName); - validateTenantConfigurationDataType(configurationKey, propertyType); final TenantConfiguration tenantConfiguration = tenantConfigurationRepository.findByKey(configurationKey.getKeyName()); - return buildTenantConfigurationValueByKey(configurationKey, propertyType, tenantConfiguration); } @@ -151,7 +154,6 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana checkAccess(configurationKeyName); final TenantConfigurationKey key = tenantConfigurationProperties.fromKeyName(configurationKeyName); - if (!key.getDataType().isAssignableFrom(propertyType)) { throw new TenantConfigurationValidatorException(String.format( "Cannot parse the database value of type %s into the type %s.", key.getDataType(), propertyType)); @@ -162,47 +164,62 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana @Override public Function pollStatusResolver() { - final Duration pollTime = DurationHelper.formattedStringToDuration( - getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class).getValue()); - final Duration overdueTime = DurationHelper.formattedStringToDuration( - getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class) - .getValue()); + final PollingTime pollingTime = new PollingTime( + getConfigurationValue(TenantConfigurationKey.POLLING_TIME, String.class).getValue()); + final Duration pollingOverdueTime = DurationHelper.fromString( + getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME, String.class).getValue()); return target -> { final Long lastTargetQuery = target.getLastTargetQuery(); if (lastTargetQuery == null) { return null; } - final LocalDateTime currentDate = LocalDateTime.now(); - final LocalDateTime lastPollDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastTargetQuery), - ZoneId.systemDefault()); - final LocalDateTime nextPollDate = lastPollDate.plus(pollTime); - final LocalDateTime overdueDate = nextPollDate.plus(overdueTime); - return new PollStatus(lastPollDate, nextPollDate, overdueDate, currentDate); + + if (!ObjectUtils.isEmpty(pollingTime.getOverrides()) && target instanceof JpaTarget jpaTarget) { + for (final PollingTime.Override override : pollingTime.getOverrides()) { + try { + if (EntityMatcher.forRsql(override.qlStr()).match(jpaTarget)) { + return pollStatus(lastTargetQuery, override.pollingInterval(), pollingOverdueTime); + } + } catch (final Exception e) { + log.warn("Error while evaluating polling override for target {}: {}", jpaTarget.getId(), e.getMessage()); + } + } + } + // returns default - no overrides or not applicable for the target + return pollStatus(lastTargetQuery, pollingTime.getPollingInterval(), pollingOverdueTime); }; } - /** - * Validates the data type of the tenant configuration. If it is possible to - * cast to the given data type. - * - * @param configurationKey the key - * @param propertyType the class - */ - private static void validateTenantConfigurationDataType(final TenantConfigurationKey configurationKey, - final Class propertyType) { + private static PollStatus pollStatus( + final long lastTargetQuery, + final PollingTime.PollingInterval pollingInterval, final Duration pollingOverdueTime) { + final LocalDateTime currentDate = LocalDateTime.now(); + final LocalDateTime lastPollDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastTargetQuery), ZoneId.systemDefault()); + LocalDateTime nextPollDate = lastPollDate.plus(pollingInterval.getInterval()); + if (pollingInterval.getDeviationPercent() > 0) { + nextPollDate = nextPollDate.plus( + pollingInterval.getInterval().toMillis() * pollingInterval.getDeviationPercent() / 100, + ChronoUnit.MILLIS); + } + final LocalDateTime overdueDate = nextPollDate.plus(pollingOverdueTime); + return new PollStatus(lastPollDate, nextPollDate, overdueDate, currentDate); + } + /** + * Validates the data type of the tenant configuration. If it is possible to cast to the given data type. + */ + private static void validateTenantConfigurationDataType(final TenantConfigurationKey configurationKey, final Class propertyType) { if (!configurationKey.getDataType().isAssignableFrom(propertyType)) { throw new TenantConfigurationValidatorException( - String.format("Cannot parse the database value of type %s into the type %s.", + String.format( + "Cannot parse the database value of type %s into the type %s.", configurationKey.getDataType(), propertyType)); } } private void checkAccess(final String configurationKeyName) { - if (TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY - .equalsIgnoreCase(configurationKeyName)) { - final SystemSecurityContext systemSecurityContext = - SystemSecurityContextHolder.getInstance().getSystemSecurityContext(); + if (AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY.equalsIgnoreCase(configurationKeyName)) { + final SystemSecurityContext systemSecurityContext = SystemSecurityContextHolder.getInstance().getSystemSecurityContext(); if (!systemSecurityContext.isCurrentThreadSystemCode() && !systemSecurityContext.hasPermission(SpPermission.READ_GATEWAY_SEC_TOKEN)) { throw new InsufficientPermissionException( @@ -211,22 +228,28 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana } } - private Map> addOrUpdateConfiguration0(Map configurations) { - List configurationList = new ArrayList<>(); + private Map> addOrUpdateConfiguration0(final Map configurations) { + final List configurationList = new ArrayList<>(); configurations.forEach((configurationKeyName, value) -> { 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())); } + configurationKey.validate(value, applicationContext); + // additional validation for specific configuration keys + if (POLLING_TIME.equals(configurationKey.getKeyName())) { + final PollingTime pollingTime = new PollingTime(value.toString()); + if (!ObjectUtils.isEmpty(pollingTime.getOverrides())) { + // validate that the QL strings are valid RSQL queries, + // nevertheless always when parse them we shall be prepared to catch exceptions if the parsers + // has been changed in non backward compatible way + pollingTime.getOverrides().forEach(override -> EntityMatcher.forRsql(override.qlStr())); + } + } - configurationKey.validate(applicationContext, value); - - JpaTenantConfiguration tenantConfiguration = tenantConfigurationRepository - .findByKey(configurationKey.getKeyName()); - + JpaTenantConfiguration tenantConfiguration = tenantConfigurationRepository.findByKey(configurationKey.getKeyName()); if (tenantConfiguration == null) { tenantConfiguration = new JpaTenantConfiguration(configurationKey.getKeyName(), value.toString()); } else { @@ -237,15 +260,12 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana configurationList.add(tenantConfiguration); }); - List jpaTenantConfigurations = tenantConfigurationRepository - .saveAll(configurationList); - + final 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(); + @SuppressWarnings("unchecked") + final Class clazzT = (Class) configurations.get(updatedTenantConfiguration.getKey()).getClass(); return TenantConfigurationValue. builder().global(false) .createdBy(updatedTenantConfiguration.getCreatedBy()) .createdAt(updatedTenantConfiguration.getCreatedAt()) @@ -257,27 +277,25 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana } private TenantConfigurationValue buildTenantConfigurationValueByKey( - final TenantConfigurationKey configurationKey, final Class propertyType, - final TenantConfiguration tenantConfiguration) { + final TenantConfigurationKey configurationKey, final Class propertyType, final TenantConfiguration tenantConfiguration) { if (tenantConfiguration != null) { return TenantConfigurationValue. builder().global(false).createdBy(tenantConfiguration.getCreatedBy()) .createdAt(tenantConfiguration.getCreatedAt()) .lastModifiedAt(tenantConfiguration.getLastModifiedAt()) .lastModifiedBy(tenantConfiguration.getLastModifiedBy()) .value(CONVERSION_SERVICE.convert(tenantConfiguration.getValue(), propertyType)).build(); - } else if (configurationKey.getDefaultValue() != null) { - return TenantConfigurationValue. builder().global(true).createdBy(null).createdAt(null) .lastModifiedAt(null).lastModifiedBy(null) .value(getGlobalConfigurationValue(configurationKey.getKeyName(), propertyType)).build(); + } else { + return null; } - return null; } /** - * Asserts that the requested configuration value change is allowed. Throws - * a {@link TenantConfigurationValueChangeNotAllowedException} otherwise. + * Asserts that the requested configuration value change is allowed. Throws a {@link TenantConfigurationValueChangeNotAllowedException} + * otherwise. * * @param key The configuration key. * @param valueChange The configuration to be validated. @@ -293,9 +311,7 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana private void assertAutoCloseValueChange(final String key, final JpaTenantConfiguration valueChange) { if (REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED.equals(key) && Boolean.TRUE.equals(getConfigurationValue(MULTI_ASSIGNMENTS_ENABLED, Boolean.class).getValue())) { - log.debug( - "The property '{}' must not be changed because the Multi-Assignments feature is currently enabled.", - key); + log.debug("The property '{}' must not be changed because the Multi-Assignments feature is currently enabled.", key); throw new TenantConfigurationValueChangeNotAllowedException(); } } @@ -308,8 +324,7 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana if (MULTI_ASSIGNMENTS_ENABLED.equals(key) && Boolean.parseBoolean(valueChange.getValue())) { JpaTenantConfiguration batchConfig = tenantConfigurationRepository.findByKey(BATCH_ASSIGNMENTS_ENABLED); if (batchConfig != null && Boolean.parseBoolean(batchConfig.getValue())) { - log.debug("The Multi-Assignments '{}' feature cannot be enabled as it contradicts with " + - "The Batch-Assignments feature, which is already enabled .", key); + log.debug("The Multi-Assignments '{}' feature cannot be enabled as it contradicts with the Batch-Assignments feature, which is already enabled .", key); throw new TenantConfigurationValueChangeNotAllowedException(); } } @@ -319,10 +334,9 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana if (BATCH_ASSIGNMENTS_ENABLED.equals(key) && Boolean.parseBoolean(valueChange.getValue())) { JpaTenantConfiguration multiConfig = tenantConfigurationRepository.findByKey(MULTI_ASSIGNMENTS_ENABLED); if (multiConfig != null && Boolean.parseBoolean(multiConfig.getValue())) { - log.debug("The Batch-Assignments '{}' feature cannot be enabled as it contradicts with " + - "The Multi-Assignments feature, which is already enabled .", key); + log.debug("The Batch-Assignments '{}' feature cannot be enabled as it contradicts with the Multi-Assignments feature, which is already enabled .", key); throw new TenantConfigurationValueChangeNotAllowedException(); } } } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementSecurityTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementSecurityTest.java index 62ed348b4..3a559dcc2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementSecurityTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementSecurityTest.java @@ -139,24 +139,7 @@ class ControllerManagementSecurityTest extends AbstractJpaIntegrationTest { */ @Test void getPollingTimePermissionsCheck() { - assertPermissions(() -> controllerManagement.getPollingTime(), List.of(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE)); - } - - /** - * Tests ControllerManagement#getMinPollingTime() method - */ - @Test - void getMinPollingTimePermissionsCheck() { - assertPermissions(() -> controllerManagement.getMinPollingTime(), List.of(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE)); - } - - /** - * Tests ControllerManagement#getMaxPollingTime() method - */ - @Test - void getMaintenanceWindowPollCountPermissionsCheck() { - assertPermissions(() -> controllerManagement.getMaintenanceWindowPollCount(), - List.of(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE)); + assertPermissions(() -> controllerManagement.getPollingTime(null), List.of(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE)); } /** @@ -168,7 +151,7 @@ class ControllerManagementSecurityTest extends AbstractJpaIntegrationTest { action.setId(1L); assertPermissions(() -> { try { - controllerManagement.getPollingTimeForAction(action); + controllerManagement.getPollingTimeForAction(action.getTarget(), action); } catch (final CancelActionNotAllowedException e) { // expected since action is not found } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TenantConfigurationManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TenantConfigurationManagementTest.java index 2c4944477..e168feca9 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TenantConfigurationManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TenantConfigurationManagementTest.java @@ -51,7 +51,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple // get the configuration from the system management final TenantConfigurationValue defaultConfigValue = tenantConfigurationManagement.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class); + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class); assertThat(defaultConfigValue.isGlobal()).isTrue(); assertThat(defaultConfigValue.getValue()).isEqualTo(envPropertyDefault); @@ -60,11 +60,11 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple final String newConfigurationValue = "thisIsAnotherTokenName"; assertThat(newConfigurationValue).isNotEqualTo(defaultConfigValue.getValue()); tenantConfigurationManagement.addOrUpdateConfiguration( - TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, newConfigurationValue); + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, newConfigurationValue); // verify that new configuration value is used final TenantConfigurationValue updatedConfigurationValue = tenantConfigurationManagement - .getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class); + .getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class); assertThat(updatedConfigurationValue.isGlobal()).isFalse(); assertThat(updatedConfigurationValue.getValue()).isEqualTo(newConfigurationValue); @@ -76,7 +76,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void updateTenantSpecificConfiguration() { - final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY; + final String configKey = TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY; final String value1 = "firstValue"; final String value2 = "secondValue"; @@ -95,14 +95,14 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple @Test void batchUpdateTenantSpecificConfiguration() { Map configuration = new HashMap<>() {{ - put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123"); + put(TenantConfigurationKey.AUTHENTICATION_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()) + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class).getValue()) .isEqualTo("token_123"); assertThat( tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, Boolean.class).getValue()) @@ -114,7 +114,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void storeAndUpdateTenantSpecificConfigurationAsBoolean() { - final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED; + final String configKey = TenantConfigurationKey.AUTHENTICATION_HEADER_ENABLED; final Boolean value1 = true; tenantConfigurationManagement.addOrUpdateConfiguration(configKey, value1); assertThat(tenantConfigurationManagement.getConfigurationValue(configKey, Boolean.class).getValue()).isEqualTo(value1); @@ -128,7 +128,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void wrongTenantConfigurationValueTypeThrowsException() { - final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED; + final String configKey = TenantConfigurationKey.AUTHENTICATION_HEADER_ENABLED; final String value1 = "thisIsNotABoolean"; // add value as String @@ -143,9 +143,9 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple @Test void batchWrongTenantConfigurationValueTypeThrowsException() { final Map configuration = new HashMap<>() {{ - put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, "token_123"); + put(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, "token_123"); put(TenantConfigurationKey.ROLLOUT_APPROVAL_ENABLED, true); - put(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, "wrong"); + put(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED, "wrong"); }}; try { @@ -154,7 +154,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple } catch (final TenantConfigurationValidatorException e) { assertThat( tenantConfigurationManagement.getConfigurationValue( - TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class).getValue()) + TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class).getValue()) .isNotEqualTo("token_123"); assertThat( tenantConfigurationManagement.getConfigurationValue( @@ -168,7 +168,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void deleteConfigurationReturnNullConfiguration() { - final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY; + final String configKey = TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY; // gateway token does not have default value so no configuration value should be available final String defaultConfigValue = tenantConfigurationManagement.getConfigurationValue(configKey, String.class).getValue(); @@ -195,7 +195,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void storesIntegerWhenStringIsExpected() { - final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY; + final String configKey = TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY; final Integer wrongDatType = 123; assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDatType)) .as("Should not have worked as integer is not a string") @@ -207,7 +207,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void storesIntegerWhenBooleanIsExpected() { - final String configKey = TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED; + final String configKey = TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED; final Integer wrongDataType = 123; assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDataType)) .as("Should not have worked as integer is not a boolean") @@ -219,7 +219,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void storesIntegerWhenPollingIntervalIsExpected() { - final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL; + final String configKey = TenantConfigurationKey.POLLING_TIME; final Integer wrongDataType = 123; assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongDataType)) .as("Should not have worked as integer is not a time field") @@ -231,7 +231,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void storesWrongFormattedStringAsPollingInterval() { - final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL; + final String configKey = TenantConfigurationKey.POLLING_TIME; final String wrongFormatted = "wrongFormatted"; assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, wrongFormatted)) .as("should not have worked as string is not a time field") @@ -243,10 +243,9 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void storesTooSmallDurationAsPollingInterval() { - final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL; + final String configKey = TenantConfigurationKey.POLLING_TIME; - final String tooSmallDuration = DurationHelper - .durationToFormattedString(DurationHelper.getDurationByTimeValues(0, 0, 1)); + final String tooSmallDuration = DurationHelper.toString(getDurationByTimeValues(0, 0, 1)); assertThatThrownBy(() -> tenantConfigurationManagement.addOrUpdateConfiguration(configKey, tooSmallDuration)) .as("Should not have worked as string has an invalid format") .isInstanceOf(TenantConfigurationValidatorException.class); @@ -257,15 +256,15 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void storesCorrectDurationAsPollingInterval() { - final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL; + final String configKey = TenantConfigurationKey.POLLING_TIME; - final Duration duration = DurationHelper.getDurationByTimeValues(1, 2, 0); + final Duration duration = getDurationByTimeValues(1, 2, 0); assertThat(duration).isEqualTo(Duration.ofHours(1).plusMinutes(2)); - tenantConfigurationManagement.addOrUpdateConfiguration(configKey, DurationHelper.durationToFormattedString(duration)); + tenantConfigurationManagement.addOrUpdateConfiguration(configKey, DurationHelper.toString(duration)); final String storedDurationString = tenantConfigurationManagement.getConfigurationValue(configKey, String.class).getValue(); - assertThat(duration).isEqualTo(DurationHelper.formattedStringToDuration(storedDurationString)); + assertThat(duration).isEqualTo(DurationHelper.fromString(storedDurationString)); } /** @@ -274,7 +273,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple @Test void requestConfigValueWithWrongType() { assertThatThrownBy(() -> tenantConfigurationManagement.getConfigurationValue( - TenantConfigurationKey.POLLING_TIME_INTERVAL, Serializable.class)) + TenantConfigurationKey.POLLING_TIME, Serializable.class)) .isInstanceOf(TenantConfigurationValidatorException.class); } @@ -297,7 +296,7 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple */ @Test void getTenantConfigurationKeyByName() { - final String configKey = TenantConfigurationKey.POLLING_TIME_INTERVAL; + final String configKey = TenantConfigurationKey.POLLING_TIME; assertThat(tenantConfigurationProperties.fromKeyName(configKey).getKeyName()).isEqualTo(configKey); } @@ -311,4 +310,8 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple .as("Expected InvalidTenantConfigurationKeyException for tenant configuration key which is not declared") .isInstanceOf(InvalidTenantConfigurationKeyException.class); } + + private static Duration getDurationByTimeValues(final long hours, final long minutes, final long seconds) { + return Duration.ofHours(hours).plusMinutes(minutes).plusSeconds(seconds); + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlUtilityTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlUtilityTest.java index 3a02de822..56934960d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlUtilityTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlUtilityTest.java @@ -411,9 +411,9 @@ class RsqlUtilityTest { VirtualPropertyReplacer setupMacroLookup() { when(securityContext.runAsSystem(Mockito.any())).thenAnswer(a -> ((Callable) a.getArgument(0)).call()); - when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class)) + when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME, String.class)) .thenReturn(TEST_POLLING_TIME_INTERVAL); - when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class)) + when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME, String.class)) .thenReturn(TEST_POLLING_OVERDUE_TIME_INTERVAL); return macroResolver; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyResolverTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyResolverTest.java index fb789b1f7..efb06d09e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyResolverTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/VirtualPropertyResolverTest.java @@ -54,9 +54,9 @@ class VirtualPropertyResolverTest { @BeforeEach void before() { - when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME_INTERVAL, String.class)) + when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_TIME, String.class)) .thenReturn(TEST_POLLING_TIME_INTERVAL); - when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME_INTERVAL, String.class)) + when(confMgmt.getConfigurationValue(TenantConfigurationKey.POLLING_OVERDUE_TIME, String.class)) .thenReturn(TEST_POLLING_OVERDUE_TIME_INTERVAL); }