From b5114081bec70339090fb1a75d15a7baaa9b68ca Mon Sep 17 00:00:00 2001 From: Bondar Bogdan <36962546+bogdan-bondar@users.noreply.github.com> Date: Tue, 15 May 2018 11:12:33 +0200 Subject: [PATCH] Integrated Maintenance Window fields in Management API and UI (#677) * Added Maintenance Window properties to API and UI * extended Management API with Maintenance Window schedule, duration, timezone and nextAt properties * extended integration tests for the above properties * extended Management UI with Maintenance Window column in Action History grid, added tooltip for next execution * general refactoring Signed-off-by: Bogdan Bondar * fixed Sonar issues Signed-off-by: Bogdan Bondar * changed the documentation help link for maintenance window Signed-off-by: Bogdan Bondar * added licence header, first refactoring after partial PR review Signed-off-by: Bogdan Bondar * changes related to PR review findings Signed-off-by: Bogdan Bondar * last PR review findings Signed-off-by: Bogdan Bondar --- .../json/model/MgmtMaintenanceWindow.java | 84 ++------ .../MgmtMaintenanceWindowRequestBody.java | 76 +++++++ .../mgmt/json/model/action/MgmtAction.java | 33 ++- .../MgmtTargetAssignmentRequestBody.java | 12 +- .../target/MgmtDistributionSetAssignment.java | 22 +- .../resource/MgmtDistributionSetResource.java | 11 +- .../mgmt/rest/resource/MgmtTargetMapper.java | 11 + .../rest/resource/MgmtTargetResource.java | 10 +- .../rest/resource/MgmtTargetResourceTest.java | 191 +++++++++++++++--- .../repository/MaintenanceScheduleHelper.java | 2 +- .../hawkbit/repository/model/Action.java | 28 ++- .../jpa/AbstractDsAssignmentStrategy.java | 2 +- .../repository/jpa/model/JpaAction.java | 39 ++-- .../test/util/AbstractIntegrationTest.java | 45 +++-- .../hawkbit/ui/common/grid/AbstractGrid.java | 49 +++-- .../actionhistory/ActionBeanQuery.java | 7 + .../actionhistory/ActionHistoryGrid.java | 24 ++- .../actionhistory/ActionStatusGrid.java | 6 +- .../management/actionhistory/ProxyAction.java | 26 ++- .../footer/MaintenanceWindowLayout.java | 3 +- .../hawkbit/ui/utils/SPUIDefinitions.java | 2 + .../src/main/resources/messages.properties | 1 + 22 files changed, 474 insertions(+), 210 deletions(-) create mode 100644 hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtMaintenanceWindowRequestBody.java diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtMaintenanceWindow.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtMaintenanceWindow.java index 878d8a412..9e7627add 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtMaintenanceWindow.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtMaintenanceWindow.java @@ -8,88 +8,32 @@ */ package org.eclipse.hawkbit.mgmt.json.model; +import java.util.concurrent.TimeUnit; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonProperty; /** - * JSON model for Management API to define the maintenance window based on a - * schedule defined as cron expression, duration in HH:mm:ss format and time - * zone as offset from UTC. + * JSON model for Management API to define the maintenance window. */ @JsonInclude(Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public class MgmtMaintenanceWindow { - - private String maintenanceSchedule; - private String maintenanceWindowDuration; - private String maintenanceWindowTimeZone; +public class MgmtMaintenanceWindow extends MgmtMaintenanceWindowRequestBody { /** - * Sets the maintenance schedule. - * - * @param maintenanceSchedule - * is the cron expression to be used for scheduling maintenance - * window(s). Expression has 6 mandatory fields and a last - * optional field: "second minute hour dayofmonth month weekday - * year". + * Time in {@link TimeUnit#MILLISECONDS} of the next maintenance window + * start */ - @JsonSetter("schedule") - public void setMaintenanceSchedule(final String maintenanceSchedule) { - this.maintenanceSchedule = maintenanceSchedule; + @JsonProperty + private long nextStartAt; + + public long getNextStartAt() { + return nextStartAt; } - /** - * Sets the maintenance window duration. - * - * @param maintenanceWindowDuration - * in HH:mm:ss format specifying the duration of a maintenance - * window, for example 00:30:00 for 30 minutes. - */ - @JsonSetter("duration") - public void setMaintenanceWindowDuration(final String maintenanceWindowDuration) { - this.maintenanceWindowDuration = maintenanceWindowDuration; - } - - /** - * Sets the maintenance window timezone. - * - * @param maintenanceWindowTimeZone - * is the time zone specified as +/-hh:mm offset from UTC. For - * example +02:00 for CET summer time and +00:00 for UTC. The - * start time of a maintenance window calculated based on the - * cron expression is relative to this time zone. - */ - @JsonSetter("timezone") - public void setMaintenanceWindowTimeZone(final String maintenanceWindowTimeZone) { - this.maintenanceWindowTimeZone = maintenanceWindowTimeZone; - } - - /** - * Returns the maintenance schedule for the {@link Action}. - * - * @return cron expression as {@link String}. - */ - public String getMaintenanceSchedule() { - return maintenanceSchedule; - } - - /** - * Returns the duration of maintenance window for the {@link Action}. - * - * @return duration in HH:mm:ss format as {@link String}. - */ - public String getMaintenanceWindowDuration() { - return maintenanceWindowDuration; - } - - /** - * Returns the timezone of maintenance window for the {@link Action}. - * - * @return the timezone offset from UTC in +/-hh:mm as {@link String}. - */ - public String getMaintenanceWindowTimeZone() { - return maintenanceWindowTimeZone; + public void setNextStartAt(final long nextStartAt) { + this.nextStartAt = nextStartAt; } } diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtMaintenanceWindowRequestBody.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtMaintenanceWindowRequestBody.java new file mode 100644 index 000000000..411af69c5 --- /dev/null +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/MgmtMaintenanceWindowRequestBody.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2018 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.mgmt.json.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request body for maintenance window PUT/POST commands, based on a schedule + * defined as cron expression, duration in HH:mm:ss format and time zone as + * offset from UTC. + * + */ +@JsonInclude(Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class MgmtMaintenanceWindowRequestBody { + @JsonProperty + private String schedule; + + @JsonProperty + private String duration; + + @JsonProperty + private String timezone; + + public String getSchedule() { + return schedule; + } + + /** + * @param schedule + * is the cron expression to be used for scheduling maintenance + * window(s). Expression has 6 mandatory fields and a last + * optional field: "second minute hour dayofmonth month weekday + * year". + */ + public void setSchedule(final String schedule) { + this.schedule = schedule; + } + + public String getDuration() { + return duration; + } + + /** + * @param duration + * in HH:mm:ss format specifying the duration of a maintenance + * window, for example 00:30:00 for 30 minutes. + */ + public void setDuration(final String duration) { + this.duration = duration; + } + + public String getTimezone() { + return timezone; + } + + /** + * @param timezone + * is the time zone specified as +/-hh:mm offset from UTC. For + * example +02:00 for CET summer time and +00:00 for UTC. The + * start time of a maintenance window calculated based on the + * cron expression is relative to this time zone. + */ + public void setTimezone(final String timezone) { + this.timezone = timezone; + } +} diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java index 98081fa33..fc6158593 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtAction.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.mgmt.json.model.action; import org.eclipse.hawkbit.mgmt.json.model.MgmtBaseEntity; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindow; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -59,6 +60,17 @@ public class MgmtAction extends MgmtBaseEntity { @JsonProperty private MgmtActionType forceType; + @JsonProperty + private MgmtMaintenanceWindow maintenanceWindow; + + public MgmtMaintenanceWindow getMaintenanceWindow() { + return maintenanceWindow; + } + + public void setMaintenanceWindow(final MgmtMaintenanceWindow maintenanceWindow) { + this.maintenanceWindow = maintenanceWindow; + } + public Long getForceTime() { return forceTime; } @@ -75,47 +87,26 @@ public class MgmtAction extends MgmtBaseEntity { this.forceType = forceType; } - /** - * @return the status - */ public String getStatus() { return status; } - /** - * @param status - * the status to set - */ public void setStatus(final String status) { this.status = status; } - /** - * @return the actionId - */ public Long getActionId() { return actionId; } - /** - * @param actionId - * the actionId to set - */ public void setActionId(final Long actionId) { this.actionId = actionId; } - /** - * @return the type - */ public String getType() { return type; } - /** - * @param type - * the type to set - */ public void setType(final String type) { this.type = type; } diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtTargetAssignmentRequestBody.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtTargetAssignmentRequestBody.java index c93254005..730b0f3be 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtTargetAssignmentRequestBody.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/distributionset/MgmtTargetAssignmentRequestBody.java @@ -8,7 +8,7 @@ */ package org.eclipse.hawkbit.mgmt.json.model.distributionset; -import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindow; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -28,10 +28,10 @@ public class MgmtTargetAssignmentRequestBody { private MgmtActionType type; /** - * {@link MgmtMaintenanceWindow} object containing schedule, duration and - * timezone. + * {@link MgmtMaintenanceWindowRequestBody} object containing schedule, + * duration and timezone. */ - private MgmtMaintenanceWindow maintenanceWindow; + private MgmtMaintenanceWindowRequestBody maintenanceWindow; public String getId() { return id; @@ -57,11 +57,11 @@ public class MgmtTargetAssignmentRequestBody { this.forcetime = forcetime; } - public MgmtMaintenanceWindow getMaintenanceWindow() { + public MgmtMaintenanceWindowRequestBody getMaintenanceWindow() { return maintenanceWindow; } - public void setMaintenanceWindow(final MgmtMaintenanceWindow maintenanceWindow) { + public void setMaintenanceWindow(final MgmtMaintenanceWindowRequestBody maintenanceWindow) { this.maintenanceWindow = maintenanceWindow; } diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignment.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignment.java index 27beacc5b..96ba199d6 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignment.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/target/MgmtDistributionSetAssignment.java @@ -4,7 +4,7 @@ package org.eclipse.hawkbit.mgmt.json.model.target; import org.eclipse.hawkbit.mgmt.json.model.MgmtId; -import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindow; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; /** @@ -16,10 +16,10 @@ public class MgmtDistributionSetAssignment extends MgmtId { private MgmtActionType type; /** - * {@link MgmtMaintenanceWindow} object defining a schedule, duration and - * timezone. + * {@link MgmtMaintenanceWindowRequestBody} object defining a schedule, + * duration and timezone. */ - private MgmtMaintenanceWindow maintenanceWindow; + private MgmtMaintenanceWindowRequestBody maintenanceWindow; public MgmtActionType getType() { return type; @@ -38,21 +38,23 @@ public class MgmtDistributionSetAssignment extends MgmtId { } /** - * Returns {@link MgmtMaintenanceWindow} for distribution set assignment. + * Returns {@link MgmtMaintenanceWindowRequestBody} for distribution set + * assignment. * - * @return {@link MgmtMaintenanceWindow}. + * @return {@link MgmtMaintenanceWindowRequestBody}. */ - public MgmtMaintenanceWindow getMaintenanceWindow() { + public MgmtMaintenanceWindowRequestBody getMaintenanceWindow() { return maintenanceWindow; } /** - * Sets {@link MgmtMaintenanceWindow} for distribution set assignment. + * Sets {@link MgmtMaintenanceWindowRequestBody} for distribution set + * assignment. * * @param maintenanceWindow - * as {@link MgmtMaintenanceWindow}. + * as {@link MgmtMaintenanceWindowRequestBody}. */ - public void setMaintenanceWindow(final MgmtMaintenanceWindow maintenanceWindow) { + public void setMaintenanceWindow(final MgmtMaintenanceWindowRequestBody maintenanceWindow) { this.maintenanceWindow = maintenanceWindow; } } diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java index f8e1329de..3cba1781f 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResource.java @@ -12,6 +12,7 @@ import java.util.Collection; import java.util.List; import java.util.stream.Collectors; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadata; import org.eclipse.hawkbit.mgmt.json.model.MgmtMetadataBodyPut; import org.eclipse.hawkbit.mgmt.json.model.PagedList; @@ -252,14 +253,16 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi { final DistributionSetAssignmentResult assignDistributionSet = this.deployManagament .assignDistributionSet(distributionSetId, assignments.stream().map(t -> { - if (t.getMaintenanceWindow() == null) { + final MgmtMaintenanceWindowRequestBody maintenanceWindow = t.getMaintenanceWindow(); + + if (maintenanceWindow == null) { return new TargetWithActionType(t.getId(), MgmtRestModelMapper.convertActionType(t.getType()), t.getForcetime()); } - final String cronSchedule = t.getMaintenanceWindow().getMaintenanceSchedule(); - final String duration = t.getMaintenanceWindow().getMaintenanceWindowDuration(); - final String timezone = t.getMaintenanceWindow().getMaintenanceWindowTimeZone(); + final String cronSchedule = maintenanceWindow.getSchedule(); + final String duration = maintenanceWindow.getDuration(); + final String timezone = maintenanceWindow.getTimezone(); MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone); diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java index 883686466..69a1ff41d 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetMapper.java @@ -19,6 +19,7 @@ import java.util.Date; import java.util.List; import java.util.stream.Collectors; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindow; import org.eclipse.hawkbit.mgmt.json.model.MgmtPollStatus; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus; @@ -194,6 +195,16 @@ public final class MgmtTargetMapper { result.setStatus(MgmtAction.ACTION_FINISHED); } + if (action.hasMaintenanceSchedule()) { + final MgmtMaintenanceWindow maintenanceWindow = new MgmtMaintenanceWindow(); + maintenanceWindow.setSchedule(action.getMaintenanceWindowSchedule()); + maintenanceWindow.setDuration(action.getMaintenanceWindowDuration()); + maintenanceWindow.setTimezone(action.getMaintenanceWindowTimeZone()); + action.getMaintenanceWindowStartTime() + .ifPresent(nextStart -> maintenanceWindow.setNextStartAt(nextStart.toInstant().toEpochMilli())); + result.setMaintenanceWindow(maintenanceWindow); + } + MgmtRestModelMapper.mapBaseToBase(result, action); result.add(linkTo(methodOn(MgmtTargetRestApi.class).getAction(targetId, action.getId())).withSelfRel()); diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java index 6cc44270c..bcc2e3d60 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResource.java @@ -15,7 +15,7 @@ import java.util.Map; import javax.validation.ValidationException; -import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindow; +import org.eclipse.hawkbit.mgmt.json.model.MgmtMaintenanceWindowRequestBody; import org.eclipse.hawkbit.mgmt.json.model.PagedList; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut; @@ -276,7 +276,7 @@ public class MgmtTargetResource implements MgmtTargetRestApi { } findTargetWithExceptionIfNotFound(controllerId); - final MgmtMaintenanceWindow maintenanceWindow = dsId.getMaintenanceWindow(); + final MgmtMaintenanceWindowRequestBody maintenanceWindow = dsId.getMaintenanceWindow(); if (maintenanceWindow == null) { return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(this.deploymentManagement @@ -284,9 +284,9 @@ public class MgmtTargetResource implements MgmtTargetRestApi { MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime()))))); } - final String cronSchedule = maintenanceWindow.getMaintenanceSchedule(); - final String duration = maintenanceWindow.getMaintenanceWindowDuration(); - final String timezone = maintenanceWindow.getMaintenanceWindowTimeZone(); + final String cronSchedule = maintenanceWindow.getSchedule(); + final String duration = maintenanceWindow.getDuration(); + final String timezone = maintenanceWindow.getTimezone(); MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone); diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java index a67876b15..701826673 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java @@ -853,17 +853,97 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest } @Test - @Description("Ensures that the expected response is return when update was cancelled.") + @Description("Ensures that the expected response is returned for update action.") + public void getUpdateAction() throws Exception { + final String knownTargetId = "targetId"; + final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + actions.get(1).getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("id", equalTo(actions.get(1).getId().intValue()))) + .andExpect(jsonPath("type", equalTo("update"))).andExpect(jsonPath("status", equalTo("pending"))) + .andExpect(jsonPath("forceType", equalTo("forced"))) + .andExpect(jsonPath("maintenanceWindow").doesNotExist()) + .andExpect(jsonPath("_links.self.href", + equalTo(generateActionSelfLink(knownTargetId, actions.get(1).getId())))) + .andExpect(jsonPath("_links.distributionset.href", + equalTo(generateActionDsLink(actions.get(1).getDistributionSet().getId())))) + .andExpect(jsonPath("_links.status.href", + equalTo(generateStatusreferenceLink(knownTargetId, actions.get(1).getId())))); + } + + @Test + @Description("Ensures that the expected response is returned for update action with maintenance window.") + public void getUpdateActionWithMaintenanceWindow() throws Exception { + final String knownTargetId = "targetId"; + final String schedule = getTestSchedule(10); + final String duration = getTestDuration(10); + final String timezone = getTestTimeZone(); + final List actions = generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(knownTargetId, + schedule, duration, timezone); + + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + actions.get(1).getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("id", equalTo(actions.get(1).getId().intValue()))) + .andExpect(jsonPath("type", equalTo("update"))).andExpect(jsonPath("status", equalTo("pending"))) + .andExpect(jsonPath("forceType", equalTo("forced"))) + .andExpect(jsonPath("maintenanceWindow.schedule", equalTo(schedule))) + .andExpect(jsonPath("maintenanceWindow.duration", equalTo(duration))) + .andExpect(jsonPath("maintenanceWindow.timezone", equalTo(timezone))) + .andExpect(jsonPath("maintenanceWindow.nextStartAt", + equalTo(actions.get(1).getMaintenanceWindowStartTime().get().toInstant().toEpochMilli()))) + .andExpect(jsonPath("_links.self.href", + equalTo(generateActionSelfLink(knownTargetId, actions.get(1).getId())))) + .andExpect(jsonPath("_links.distributionset.href", + equalTo(generateActionDsLink(actions.get(1).getDistributionSet().getId())))) + .andExpect(jsonPath("_links.status.href", + equalTo(generateStatusreferenceLink(knownTargetId, actions.get(1).getId())))); + } + + @Test + @Description("Ensures that the expected response is returned when update action was cancelled.") public void getCancelAction() throws Exception { final String knownTargetId = "targetId"; final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + actions.get(0).getId())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("id", equalTo(actions.get(0).getId().intValue()))) + .andExpect(jsonPath("type", equalTo("cancel"))).andExpect(jsonPath("status", equalTo("pending"))) + .andExpect(jsonPath("forceType", equalTo("forced"))) + .andExpect(jsonPath("maintenanceWindow").doesNotExist()) + .andExpect(jsonPath("_links.self.href", + equalTo(generateActionSelfLink(knownTargetId, actions.get(0).getId())))) + .andExpect(jsonPath("_links.canceledaction.href", + equalTo(generateCanceledactionreferenceLink(knownTargetId, actions.get(0))))) + .andExpect(jsonPath("_links.status.href", + equalTo(generateStatusreferenceLink(knownTargetId, actions.get(0).getId())))); + } + + @Test + @Description("Ensures that the expected response is returned when update action with maintenance window was cancelled.") + public void getCancelActionWithMaintenanceWindow() throws Exception { + final String knownTargetId = "targetId"; + final String schedule = getTestSchedule(10); + final String duration = getTestDuration(10); + final String timezone = getTestTimeZone(); + final List actions = generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(knownTargetId, + schedule, duration, timezone); + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + actions.get(0).getId())) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(jsonPath("id", equalTo(actions.get(0).getId().intValue()))) .andExpect(jsonPath("forceType", equalTo("forced"))).andExpect(jsonPath("type", equalTo("cancel"))) .andExpect(jsonPath("status", equalTo("pending"))) + .andExpect(jsonPath("maintenanceWindow.schedule", equalTo(schedule))) + .andExpect(jsonPath("maintenanceWindow.duration", equalTo(duration))) + .andExpect(jsonPath("maintenanceWindow.timezone", equalTo(timezone))) + .andExpect(jsonPath("maintenanceWindow.nextStartAt", + equalTo(actions.get(0).getMaintenanceWindowStartTime().get().toInstant().toEpochMilli()))) .andExpect(jsonPath("_links.self.href", equalTo(generateActionSelfLink(knownTargetId, actions.get(0).getId())))) .andExpect(jsonPath("_links.canceledaction.href", @@ -896,6 +976,44 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(2))); } + @Test + @Description("Ensures that the expected response of getting actions with maintenance window of a target is returned.") + public void getMultipleActionsWithMaintenanceWindow() throws Exception { + final String knownTargetId = "targetId"; + final String schedule = getTestSchedule(10); + final String duration = getTestDuration(10); + final String timezone = getTestTimeZone(); + final List actions = generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(knownTargetId, + schedule, duration, timezone); + + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + + MgmtRestConstants.TARGET_V1_ACTIONS).param(MgmtRestConstants.REQUEST_PARAMETER_SORTING, "ID:ASC")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("content.[1].id", equalTo(actions.get(1).getId().intValue()))) + .andExpect(jsonPath("content.[1].type", equalTo("update"))) + .andExpect(jsonPath("content.[1].status", equalTo("pending"))) + .andExpect(jsonPath("content.[1].maintenanceWindow.schedule", equalTo(schedule))) + .andExpect(jsonPath("content.[1].maintenanceWindow.duration", equalTo(duration))) + .andExpect(jsonPath("content.[1].maintenanceWindow.timezone", equalTo(timezone))) + .andExpect(jsonPath("content.[1].maintenanceWindow.nextStartAt", + equalTo(actions.get(1).getMaintenanceWindowStartTime().get().toInstant().toEpochMilli()))) + .andExpect(jsonPath("content.[1]._links.self.href", + equalTo(generateActionSelfLink(knownTargetId, actions.get(1).getId())))) + .andExpect(jsonPath("content.[0].id", equalTo(actions.get(0).getId().intValue()))) + .andExpect(jsonPath("content.[0].type", equalTo("cancel"))) + .andExpect(jsonPath("content.[0].status", equalTo("pending"))) + .andExpect(jsonPath("content.[0].maintenanceWindow.schedule", equalTo(schedule))) + .andExpect(jsonPath("content.[0].maintenanceWindow.duration", equalTo(duration))) + .andExpect(jsonPath("content.[0].maintenanceWindow.timezone", equalTo(timezone))) + .andExpect(jsonPath("content.[0].maintenanceWindow.nextStartAt", + equalTo(actions.get(0).getMaintenanceWindowStartTime().get().toInstant().toEpochMilli()))) + .andExpect(jsonPath("content.[0]._links.self.href", + equalTo(generateActionSelfLink(knownTargetId, actions.get(0).getId())))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_TOTAL, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_SIZE, equalTo(2))) + .andExpect(jsonPath(JSON_PATH_PAGED_LIST_CONTENT, hasSize(2))); + } + @Test @Description("Verifies that the API returns the status list with expected content.") public void getMultipleActionStatus() throws Exception { @@ -1054,6 +1172,10 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + actionId; } + private String generateActionDsLink(final Long dsId) { + return "http://localhost" + MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + dsId; + } + private String generateCanceledactionreferenceLink(final String knownTargetId, final Action action) { return "http://localhost" + MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + action.getId(); @@ -1067,20 +1189,33 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest private List generateTargetWithTwoUpdatesWithOneOverride(final String knownTargetId) throws InterruptedException { + return generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(knownTargetId, null, null, null); + } + private List generateTargetWithTwoUpdatesWithOneOverrideWithMaintenanceWindow(final String knownTargetId, + final String schedule, final String duration, final String timezone) throws InterruptedException { final Target target = testdataFactory.createTarget(knownTargetId); - final List targets = Arrays.asList(target); final Iterator sets = testdataFactory.createDistributionSets(2).iterator(); final DistributionSet one = sets.next(); final DistributionSet two = sets.next(); // Update - final List updatedTargets = assignDistributionSet(one, targets).getAssignedEntity(); - // 2nd update - // sleep 10ms to ensure that we can sort by reportedAt - Thread.sleep(10); - assignDistributionSet(two, updatedTargets); + if (schedule == null) { + final List updatedTargets = assignDistributionSet(one, Arrays.asList(target)).getAssignedEntity(); + // 2nd update + // sleep 10ms to ensure that we can sort by reportedAt + Thread.sleep(10); + assignDistributionSet(two, updatedTargets); + } else { + final List updatedTargets = assignDistributionSetWithMaintenanceWindow(one.getId(), + target.getControllerId(), schedule, duration, timezone).getAssignedEntity(); + // 2nd update + // sleep 10ms to ensure that we can sort by reportedAt + Thread.sleep(10); + assignDistributionSetWithMaintenanceWindow(two.getId(), updatedTargets.get(0).getControllerId(), schedule, + duration, timezone); + } // two updates, one cancellation final List actions = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE) @@ -1090,26 +1225,6 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest return actions; } - @Test - public void getUpdateAction() throws Exception { - final String knownTargetId = "targetId"; - final List actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId); - - mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/" - + MgmtRestConstants.TARGET_V1_ACTIONS + "/" + actions.get(1).getId())) - .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) - .andExpect(jsonPath("id", equalTo(actions.get(1).getId().intValue()))) - .andExpect(jsonPath("type", equalTo("update"))).andExpect(jsonPath("status", equalTo("pending"))) - .andExpect(jsonPath("forceType", equalTo("forced"))) - .andExpect(jsonPath("_links.self.href", - equalTo(generateActionSelfLink(knownTargetId, actions.get(1).getId())))) - .andExpect(jsonPath("_links.distributionset.href", - equalTo("http://localhost/rest/v1/distributionsets/" - + actions.get(1).getDistributionSet().getId()))) - .andExpect(jsonPath("_links.status.href", - equalTo(generateStatusreferenceLink(knownTargetId, actions.get(1).getId())))); - } - @Test @Description("Verfies that an action is switched from soft to forced if requested by management API") public void updateAction() throws Exception { @@ -1266,6 +1381,28 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest .andExpect(status().isOk()); } + @Test + @Description("Assigns distribution set to target with maintenance window next execution start (should be ignored, calculated automaticaly based on schedule, duration and timezone)") + public void assignDistributionSetToTargetWithMaintenanceWindowNextExecutionStart() throws Exception { + + final Target target = testdataFactory.createTarget("fsdfsd"); + final DistributionSet set = testdataFactory.createDistributionSet("one"); + final long nextExecutionStart = System.currentTimeMillis(); + + final String body = new JSONObject().put("id", set.getId()) + .put("maintenanceWindow", getMaintenanceWindowWithNextStart(getTestSchedule(10), getTestDuration(10), + getTestTimeZone(), nextExecutionStart)) + .toString(); + + mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/assignedDS") + .content(body).contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + target.getControllerId() + "/" + + MgmtRestConstants.TARGET_V1_ACTIONS)).andExpect(status().isOk()).andDo(MockMvcResultPrinter.print()) + .andExpect(jsonPath("content.[0].maintenanceWindow.nextStartAt", not(nextExecutionStart))); + } + @Test @Description("Assigns distribution set to target with last maintenance window scheduled before current time.") public void assignDistributionSetToTargetWithMaintenanceWindowEndTimeBeforeStartTime() throws Exception { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/MaintenanceScheduleHelper.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/MaintenanceScheduleHelper.java index 92975ccbf..e5d3f1de8 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/MaintenanceScheduleHelper.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/MaintenanceScheduleHelper.java @@ -55,7 +55,7 @@ public final class MaintenanceScheduleHelper { * start time of a maintenance window calculated based on the * cron expression is relative to this time zone. * - * @return {@link Optional} of the next available window. In + * @return { @link Optional} of the next available window. In * case there is none, or there are maintenance window validation * errors, returns empty value. * diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java index 689ce3c21..42835a0ea 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java @@ -8,6 +8,8 @@ */ package org.eclipse.hawkbit.repository.model; +import java.time.ZonedDateTime; +import java.util.Optional; import java.util.concurrent.TimeUnit; /** @@ -18,7 +20,7 @@ public interface Action extends TenantAwareBaseEntity { /** * Maximum length of controllerId. */ - int MAINTENANCE_SCHEDULE_CRON_LENGTH = 128; + int MAINTENANCE_WINDOW_SCHEDULE_LENGTH = 128; /** * Maximum length of controllerId. @@ -81,6 +83,21 @@ public interface Action extends TenantAwareBaseEntity { */ Rollout getRollout(); + /** + * @return maintenance window schedule related to this {@link Action}. + */ + String getMaintenanceWindowSchedule(); + + /** + * @return maintenance window duration related to this {@link Action}. + */ + String getMaintenanceWindowDuration(); + + /** + * @return maintenance window time zone related to this {@link Action}. + */ + String getMaintenanceWindowTimeZone(); + /** * checks if the {@link #getForcedTime()} is hit by the given * {@code hitTimeMillis}, by means if the given milliseconds are greater @@ -215,6 +232,15 @@ public interface Action extends TenantAwareBaseEntity { TIMEFORCED; } + /** + * Returns the start time of next available maintenance window for the + * {@link Action} as {@link ZonedDateTime}. If a maintenance window is + * already active, the start time of currently active window is returned. + * + * @return the start time as { @link Optional}. + */ + Optional getMaintenanceWindowStartTime(); + /** * The method checks whether the action has a maintenance schedule defined * for it. A maintenance schedule defines a set of maintenance windows diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java index bd835f805..ce0bfb2cc 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java @@ -229,7 +229,7 @@ public abstract class AbstractDsAssignmentStrategy { actionForTarget.setActive(true); actionForTarget.setTarget(target); actionForTarget.setDistributionSet(set); - actionForTarget.setMaintenanceSchedule(targetWithActionType.getMaintenanceSchedule()); + actionForTarget.setMaintenanceWindowSchedule(targetWithActionType.getMaintenanceSchedule()); actionForTarget.setMaintenanceWindowDuration(targetWithActionType.getMaintenanceWindowDuration()); actionForTarget.setMaintenanceWindowTimeZone(targetWithActionType.getMaintenanceWindowTimeZone()); return actionForTarget; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java index dcfa915d0..313c275c3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java @@ -117,8 +117,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio @JoinColumn(name = "rollout", updatable = false, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_action_rollout")) private JpaRollout rollout; - @Column(name = "maintenance_cron_schedule", updatable = false, length = Action.MAINTENANCE_SCHEDULE_CRON_LENGTH) - private String maintenanceSchedule; + @Column(name = "maintenance_cron_schedule", updatable = false, length = Action.MAINTENANCE_WINDOW_SCHEDULE_LENGTH) + private String maintenanceWindowSchedule; @Column(name = "maintenance_duration", updatable = false, length = Action.MAINTENANCE_WINDOW_DURATION_LENGTH) private String maintenanceWindowDuration; @@ -231,14 +231,24 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio // there is no action deletion } + @Override + public String getMaintenanceWindowSchedule() { + return maintenanceWindowSchedule; + } + /** * Sets the maintenance schedule. * - * @param maintenanceSchedule + * @param maintenanceWindowSchedule * is a cron expression to be used for scheduling. */ - public void setMaintenanceSchedule(final String maintenanceSchedule) { - this.maintenanceSchedule = maintenanceSchedule; + public void setMaintenanceWindowSchedule(final String maintenanceWindowSchedule) { + this.maintenanceWindowSchedule = maintenanceWindowSchedule; + } + + @Override + public String getMaintenanceWindowDuration() { + return maintenanceWindowDuration; } /** @@ -252,6 +262,11 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio this.maintenanceWindowDuration = maintenanceWindowDuration; } + @Override + public String getMaintenanceWindowTimeZone() { + return maintenanceWindowTimeZone; + } + /** * Sets the time zone to be used for maintenance window. * @@ -265,15 +280,9 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio this.maintenanceWindowTimeZone = maintenanceWindowTimeZone; } - /** - * Returns the start time of next available maintenance window for the - * {@link Action} as {@link ZonedDateTime}. If a maintenance window is - * already active, the start time of currently active window is returned. - * - * @return the start time as {@link Optional}. - */ + @Override public Optional getMaintenanceWindowStartTime() { - return MaintenanceScheduleHelper.getNextMaintenanceWindow(maintenanceSchedule, maintenanceWindowDuration, + return MaintenanceScheduleHelper.getNextMaintenanceWindow(maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone); } @@ -282,7 +291,7 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio * the {@link Action} as {@link ZonedDateTime}. If a maintenance window is * already active, the end time of currently active window is returned. * - * @return the end time of window as {@link Optional}. + * @return the end time of window as { @link Optional}. */ private Optional getMaintenanceWindowEndTime() { return getMaintenanceWindowStartTime() @@ -291,7 +300,7 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio @Override public boolean hasMaintenanceSchedule() { - return this.maintenanceSchedule != null; + return this.maintenanceWindowSchedule != null; } @Override diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index 50d91cbb2..dcfe57b90 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -225,7 +225,7 @@ public abstract class AbstractIntegrationTest { } }; - protected DistributionSetAssignmentResult assignDistributionSet(final Long dsID, final String controllerId) { + protected DistributionSetAssignmentResult assignDistributionSet(final long dsID, final String controllerId) { return deploymentManagement.assignDistributionSet(dsID, Arrays.asList( new TargetWithActionType(controllerId, ActionType.FORCED, RepositoryModelConstants.NO_FORCE_TIME))); } @@ -253,18 +253,26 @@ public abstract class AbstractIntegrationTest { * start time of a maintenance window calculated based on the * cron expression is relative to this time zone * - * @return result of the assignment as - * {@link DistributionSetAssignmentResult}. + * @return result of the assignment as { @link + * DistributionSetAssignmentResult}. */ - protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindow(final Long dsID, - final String controllerId, final String maintenanceSchedule, final String maintenanceWindowDuration, + protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindow(final long dsID, + final String controllerId, final String maintenanceWindowSchedule, final String maintenanceWindowDuration, final String maintenanceWindowTimeZone) { return deploymentManagement.assignDistributionSet(dsID, Arrays.asList(new TargetWithActionType(controllerId, ActionType.FORCED, - RepositoryModelConstants.NO_FORCE_TIME, maintenanceSchedule, maintenanceWindowDuration, + RepositoryModelConstants.NO_FORCE_TIME, maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone))); } + protected DistributionSetAssignmentResult assignDistributionSetWithMaintenanceWindowTimeForced(final long dsID, + final String controllerId, final String maintenanceWindowSchedule, final String maintenanceWindowDuration, + final String maintenanceWindowTimeZone) { + return deploymentManagement.assignDistributionSet(dsID, + Arrays.asList(new TargetWithActionType(controllerId, ActionType.TIMEFORCED, System.currentTimeMillis(), + maintenanceWindowSchedule, maintenanceWindowDuration, maintenanceWindowTimeZone))); + } + protected DistributionSetAssignmentResult assignDistributionSet(final DistributionSet pset, final List targets) { return deploymentManagement.assignDistributionSet(pset.getId(), @@ -275,11 +283,17 @@ public abstract class AbstractIntegrationTest { return assignDistributionSet(pset, Arrays.asList(target)); } - protected DistributionSetMetadata createDistributionSetMetadata(final Long dsId, final MetaData md) { + protected DistributionSetAssignmentResult assignDistributionSetTimeForced(final DistributionSet pset, + final Target target) { + return deploymentManagement.assignDistributionSet(pset.getId(), Arrays.asList( + new TargetWithActionType(target.getControllerId(), ActionType.TIMEFORCED, System.currentTimeMillis()))); + } + + protected DistributionSetMetadata createDistributionSetMetadata(final long dsId, final MetaData md) { return createDistributionSetMetadata(dsId, Collections.singletonList(md)).get(0); } - protected List createDistributionSetMetadata(final Long dsId, final List md) { + protected List createDistributionSetMetadata(final long dsId, final List md) { return distributionSetManagement.createMetaData(dsId, md); } @@ -378,23 +392,23 @@ public abstract class AbstractIntegrationTest { * * @return {@link String} containing a valid cron expression. */ - public static String getTestSchedule(final int minutesToAdd) { + protected static String getTestSchedule(final int minutesToAdd) { ZonedDateTime currentTime = ZonedDateTime.now(); currentTime = currentTime.plusMinutes(minutesToAdd); return String.format("0 %d %d %d %d ? %d", currentTime.getMinute(), currentTime.getHour(), currentTime.getDayOfMonth(), currentTime.getMonthValue(), currentTime.getYear()); } - public static String getTestDuration(final int duration) { + protected static String getTestDuration(final int duration) { return String.format("%02d:%02d:00", duration / 60, duration % 60); } - public static String getTestTimeZone() { + protected static String getTestTimeZone() { final ZonedDateTime currentTime = ZonedDateTime.now(); return currentTime.getOffset().getId().replace("Z", "+00:00"); } - public static Map getMaintenanceWindow(final String schedule, final String duration, + protected static Map getMaintenanceWindow(final String schedule, final String duration, final String timezone) { final Map maintenanceWindowMap = new HashMap<>(); maintenanceWindowMap.put("schedule", schedule); @@ -402,4 +416,11 @@ public abstract class AbstractIntegrationTest { maintenanceWindowMap.put("timezone", timezone); return maintenanceWindowMap; } + + protected static Map getMaintenanceWindowWithNextStart(final String schedule, final String duration, + final String timezone, final long nextStartAt) { + final Map maintenanceWindowMap = getMaintenanceWindow(schedule, duration, timezone); + maintenanceWindowMap.put("nextStartAt", String.valueOf(nextStartAt)); + return maintenanceWindowMap; + } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java index 90999aba6..6a07c5928 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/grid/AbstractGrid.java @@ -8,12 +8,16 @@ */ package org.eclipse.hawkbit.ui.common.grid; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Locale; import java.util.Objects; +import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.components.RefreshableContainer; +import org.eclipse.hawkbit.ui.management.actionhistory.ProxyAction; +import org.eclipse.hawkbit.ui.management.actionhistory.ProxyActionStatus; import org.eclipse.hawkbit.ui.utils.SPDateTimeUtil; import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; @@ -284,8 +288,8 @@ public abstract class AbstractGrid extends Grid implements Re /** * Template method invoked by {@link this#addNewContainerDS()} for adding - * properties to the container (usually by invoing - * {@link Container#addContainerProperty(Object, Class, Object))}) + * properties to the container (usually by invoking { @link + * Container#addContainerProperty(Object, Class, Object))}) */ protected abstract void addContainerProperties(); @@ -581,7 +585,7 @@ public abstract class AbstractGrid extends Grid implements Re * CellStyleGenerator that concerns about alignment in the grid cells. */ protected static class AlignCellStyleGenerator implements CellStyleGenerator { - private static final long serialVersionUID = 5573570647129792429L; + private static final long serialVersionUID = 1L; private final String[] left; private final String[] center; @@ -621,29 +625,40 @@ public abstract class AbstractGrid extends Grid implements Re } /** - * Adds a tooltip to the 'Date and time' column in detailed format. + * Adds a tooltip to the 'Date and time' and 'Maintenance Window' columns in + * detailed format. */ - public static class ModifiedTimeTooltipGenerator implements CellDescriptionGenerator { - private static final long serialVersionUID = -6617911967167729195L; + protected static class TooltipGenerator implements CellDescriptionGenerator { + private static final long serialVersionUID = 1L; - private final String datePropertyId; + private final VaadinMessageSource i18n; - /** - * Constructor. - * - * @param datePropertyId - */ - public ModifiedTimeTooltipGenerator(final String datePropertyId) { - this.datePropertyId = datePropertyId; + public TooltipGenerator(final VaadinMessageSource i18n) { + this.i18n = i18n; } @Override public String getDescription(final CellReference cell) { - if (!datePropertyId.equals(cell.getPropertyId())) { + final String propertyId = (String) cell.getPropertyId(); + switch (propertyId) { + case ProxyAction.PXY_ACTION_LAST_MODIFIED_AT: + case ProxyActionStatus.PXY_AS_CREATED_AT: + final Long timestamp = (Long) cell.getItem().getItemProperty(propertyId).getValue(); + return SPDateTimeUtil.getFormattedDate(timestamp); + + case ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW: + final Action action = (Action) cell.getItem().getItemProperty(ProxyAction.PXY_ACTION).getValue(); + return action.getMaintenanceWindowStartTime().map(this::getFormattedNextMaintenanceWindow).orElse(null); + + default: return null; } - final Long timestamp = (Long) cell.getItem().getItemProperty(datePropertyId).getValue(); - return SPDateTimeUtil.getFormattedDate(timestamp); + } + + private String getFormattedNextMaintenanceWindow(final ZonedDateTime nextAt) { + final long nextAtMilli = nextAt.toInstant().toEpochMilli(); + return i18n.getMessage("tooltip.next.maintenancewindow", + SPDateTimeUtil.getFormattedDate(nextAtMilli, SPUIDefinitions.LAST_QUERY_DATE_FORMAT_SHORT)); } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionBeanQuery.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionBeanQuery.java index 7d087c99f..d862f4196 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionBeanQuery.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionBeanQuery.java @@ -115,12 +115,19 @@ public class ActionBeanQuery extends AbstractBeanQuery { proxyAction.setLastModifiedAt(action.getLastModifiedAt()); proxyAction.setRolloutName(action.getRollout() != null ? action.getRollout().getName() : ""); proxyAction.setStatus(action.getStatus()); + proxyAction.setMaintenanceWindow( + action.hasMaintenanceSchedule() ? buildMaintenanceWindowDisplayText(action) : ""); proxyActions.add(proxyAction); } return proxyActions; } + private static String buildMaintenanceWindowDisplayText(final Action action) { + return action.getMaintenanceWindowSchedule() + "/" + action.getMaintenanceWindowDuration() + "/" + + action.getMaintenanceWindowTimeZone(); + } + /** * Generates a virtual IsActiveDecoration for the presentation layer that is * calculated from {@link Action#isActive()} and diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java index 3addfaa4c..6838a3dbf 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionHistoryGrid.java @@ -78,13 +78,14 @@ public class ActionHistoryGrid extends AbstractGrid { private static final Object[] maxColumnOrder = new Object[] { ProxyAction.PXY_ACTION_IS_ACTIVE_DECO, ProxyAction.PXY_ACTION_ID, ProxyAction.PXY_ACTION_DS_NAME_VERSION, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT, - ProxyAction.PXY_ACTION_STATUS, ProxyAction.PXY_ACTION_ROLLOUT_NAME, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, - VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT }; + ProxyAction.PXY_ACTION_STATUS, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW, + ProxyAction.PXY_ACTION_ROLLOUT_NAME, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, + VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT }; private static final Object[] minColumnOrder = new Object[] { ProxyAction.PXY_ACTION_IS_ACTIVE_DECO, ProxyAction.PXY_ACTION_DS_NAME_VERSION, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT, - ProxyAction.PXY_ACTION_STATUS, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, - VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT }; + ProxyAction.PXY_ACTION_STATUS, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW, VIRT_PROP_FORCED, + VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT }; private static final String[] leftAlignedColumns = new String[] { VIRT_PROP_TIMEFORCED }; @@ -99,7 +100,7 @@ public class ActionHistoryGrid extends AbstractGrid { private Target selectedTarget; private final AlignCellStyleGenerator alignGenerator; - private final ModifiedTimeTooltipGenerator modTimetooltipGenerator; + private final TooltipGenerator tooltipGenerator; private final Map states; private final Map activeStates; @@ -139,7 +140,7 @@ public class ActionHistoryGrid extends AbstractGrid { activeStates = conf .createActiveStatusLabelConfig(UIComponentIdProvider.ACTION_HISTORY_TABLE_ACTIVESTATE_LABEL_ID); alignGenerator = new AlignCellStyleGenerator(leftAlignedColumns, centerAlignedColumns, rightAlignedColumns); - modTimetooltipGenerator = new ModifiedTimeTooltipGenerator(ProxyAction.PXY_ACTION_LAST_MODIFIED_AT); + tooltipGenerator = new TooltipGenerator(i18n); init(); } @@ -208,6 +209,7 @@ public class ActionHistoryGrid extends AbstractGrid { rawCont.addContainerProperty(ProxyAction.PXY_ACTION_ID, String.class, null, true, true); rawCont.addContainerProperty(ProxyAction.PXY_ACTION_ROLLOUT_NAME, String.class, null, true, true); + rawCont.addContainerProperty(ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW, String.class, null, true, false); } @Override @@ -436,11 +438,14 @@ public class ActionHistoryGrid extends AbstractGrid { getColumn(VIRT_PROP_ACTION_CANCEL).setHidable(false); getColumn(VIRT_PROP_ACTION_FORCE).setHidable(false); getColumn(VIRT_PROP_ACTION_FORCE_QUIT).setHidable(false); + + getColumn(ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW).setHidden(true); + getColumn(ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW).setHidable(true); } @Override protected CellDescriptionGenerator getDescriptionGenerator() { - return modTimetooltipGenerator; + return tooltipGenerator; } @Override @@ -451,6 +456,8 @@ public class ActionHistoryGrid extends AbstractGrid { getColumn(ProxyAction.PXY_ACTION_DS_NAME_VERSION).setHeaderCaption(SPUIDefinitions.ACTION_HIS_TBL_DIST); getColumn(ProxyAction.PXY_ACTION_LAST_MODIFIED_AT).setHeaderCaption(SPUIDefinitions.ACTION_HIS_TBL_DATETIME); getColumn(ProxyAction.PXY_ACTION_STATUS).setHeaderCaption(SPUIDefinitions.ACTION_HIS_TBL_STATUS); + getColumn(ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW) + .setHeaderCaption(SPUIDefinitions.ACTION_HIS_TBL_MAINTENANCE_WINDOW); getColumn(VIRT_PROP_FORCED).setHeaderCaption(String.valueOf(forceClientRefreshToggle)); forceClientRefreshToggle = !forceClientRefreshToggle; @@ -463,8 +470,9 @@ public class ActionHistoryGrid extends AbstractGrid { protected void setColumnExpandRatio() { setColumnsSize(50.0, 50.0, ProxyAction.PXY_ACTION_IS_ACTIVE_DECO); setColumnsSize(107.0, 500.0, ProxyAction.PXY_ACTION_DS_NAME_VERSION); - setColumnsSize(100.0, 120.0, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT); + setColumnsSize(100.0, 130.0, ProxyAction.PXY_ACTION_LAST_MODIFIED_AT); setColumnsSize(53.0, 55.0, ProxyAction.PXY_ACTION_STATUS); + setColumnsSize(150.0, 200.0, ProxyAction.PXY_ACTION_MAINTENANCE_WINDOW); setColumnsSize(FIXED_PIX_MIN, FIXED_PIX_MIN, VIRT_PROP_FORCED, VIRT_PROP_TIMEFORCED, VIRT_PROP_ACTION_CANCEL, VIRT_PROP_ACTION_FORCE, VIRT_PROP_ACTION_FORCE_QUIT); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java index a7e18f855..959758465 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ActionStatusGrid.java @@ -36,7 +36,7 @@ public class ActionStatusGrid extends AbstractGrid { private static final String[] centerAlignedColumns = new String[] { ProxyActionStatus.PXY_AS_STATUS }; private final AlignCellStyleGenerator alignGenerator; - private final ModifiedTimeTooltipGenerator modTimetooltipGenerator; + private final TooltipGenerator tooltipGenerator; private final Map states; @@ -58,7 +58,7 @@ public class ActionStatusGrid extends AbstractGrid { final LabelConfig conf = new ActionHistoryGrid.LabelConfig(); states = conf.createStatusLabelConfig(i18n, UIComponentIdProvider.ACTION_STATUS_GRID_STATUS_LABEL_ID); alignGenerator = new AlignCellStyleGenerator(leftAlignedColumns, centerAlignedColumns, null); - modTimetooltipGenerator = new ModifiedTimeTooltipGenerator(ProxyActionStatus.PXY_AS_CREATED_AT); + tooltipGenerator = new TooltipGenerator(i18n); init(); } @@ -153,7 +153,7 @@ public class ActionStatusGrid extends AbstractGrid { @Override protected CellDescriptionGenerator getDescriptionGenerator() { - return modTimetooltipGenerator; + return tooltipGenerator; } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ProxyAction.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ProxyAction.java index 5f6e07dce..e6b9fa80e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ProxyAction.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/actionhistory/ProxyAction.java @@ -26,6 +26,7 @@ public class ProxyAction implements Serializable { public static final String PXY_ACTION = "action"; public static final String PXY_ACTION_LAST_MODIFIED_AT = "lastModifiedAt"; public static final String PXY_ACTION_ROLLOUT_NAME = "rolloutName"; + public static final String PXY_ACTION_MAINTENANCE_WINDOW = "maintenanceWindow"; private Action.Status status; private boolean isActive; @@ -35,6 +36,15 @@ public class ProxyAction implements Serializable { private Action action; private Long lastModifiedAt; private String rolloutName; + private String maintenanceWindow; + + public String getMaintenanceWindow() { + return maintenanceWindow; + } + + public void setMaintenanceWindow(final String maintenanceWindow) { + this.maintenanceWindow = maintenanceWindow; + } /** * Get id for the entry. @@ -51,7 +61,7 @@ public class ProxyAction implements Serializable { * @param id * of the action entry. */ - public void setId(Long id) { + public void setId(final Long id) { this.id = id; } @@ -70,7 +80,7 @@ public class ProxyAction implements Serializable { * @param status * literal */ - public void setStatus(Action.Status status) { + public void setStatus(final Action.Status status) { this.status = status; } @@ -91,7 +101,7 @@ public class ProxyAction implements Serializable { * true if the action is active, otherwise * false */ - public void setActive(boolean isActive) { + public void setActive(final boolean isActive) { this.isActive = isActive; } @@ -113,7 +123,7 @@ public class ProxyAction implements Serializable { * @param isActiveDecoration * pre-calculated literal */ - public void setIsActiveDecoration(IsActiveDecoration isActiveDecoration) { + public void setIsActiveDecoration(final IsActiveDecoration isActiveDecoration) { this.isActiveDecoration = isActiveDecoration; } @@ -133,7 +143,7 @@ public class ProxyAction implements Serializable { * @param dsNameVersion * combined value */ - public void setDsNameVersion(String dsNameVersion) { + public void setDsNameVersion(final String dsNameVersion) { this.dsNameVersion = dsNameVersion; } @@ -151,7 +161,7 @@ public class ProxyAction implements Serializable { * * @param action */ - public void setAction(Action action) { + public void setAction(final Action action) { this.action = action; } @@ -170,7 +180,7 @@ public class ProxyAction implements Serializable { * @param lastModifiedAt * raw long-value for lastModifiedAt-date */ - public void setLastModifiedAt(Long lastModifiedAt) { + public void setLastModifiedAt(final Long lastModifiedAt) { this.lastModifiedAt = lastModifiedAt; } @@ -188,7 +198,7 @@ public class ProxyAction implements Serializable { * * @param rolloutName */ - public void setRolloutName(String rolloutName) { + public void setRolloutName(final String rolloutName) { this.rolloutName = rolloutName; } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/footer/MaintenanceWindowLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/footer/MaintenanceWindowLayout.java index 64555d509..9cd70f3d9 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/footer/MaintenanceWindowLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/management/footer/MaintenanceWindowLayout.java @@ -84,7 +84,7 @@ public class MaintenanceWindowLayout extends VerticalLayout { * Text field to specify the schedule. */ private void createMaintenanceScheduleControl() { - schedule = new TextFieldBuilder(Action.MAINTENANCE_SCHEDULE_CRON_LENGTH) + schedule = new TextFieldBuilder(Action.MAINTENANCE_WINDOW_SCHEDULE_LENGTH) .id(UIComponentIdProvider.MAINTENANCE_WINDOW_SCHEDULE_ID) .caption(i18n.getMessage("caption.maintenancewindow.schedule")).validator(new CronValidator()) .prompt("0 0 3 ? * 6").required(true, i18n).buildTextComponent(); @@ -259,6 +259,7 @@ public class MaintenanceWindowLayout extends VerticalLayout { schedule.setValue(""); duration.setValue(""); timeZone.setValue(getClientTimeZone()); + scheduleTranslator.setValue(i18n.getMessage(CRON_VALIDATION_ERROR)); } /** diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIDefinitions.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIDefinitions.java index 62e871c51..7a124c000 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIDefinitions.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIDefinitions.java @@ -561,6 +561,8 @@ public final class SPUIDefinitions { */ public static final String SP_BUTTON_STATUS_STYLE = "targetStatusBtn"; + public static final String ACTION_HIS_TBL_MAINTENANCE_WINDOW = "Maintenance Window"; + /** * /** Constructor. */ diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index fe853009a..19d356724 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -251,6 +251,7 @@ tooltip.check.for.mandatory=Check to make Mandatory tooltip.artifact.icon=Show Artifact Details tooltip.click.to.edit = Click to edit tooltip.metadata.icon = Manage Metadata +tooltip.next.maintenancewindow = next on {0} #rollout action tooltip.rollout.run = Run tooltip.rollout.pause = Pause