feat: allow a target to set offline assigned distribution set (#1620)
* feat: allow a target to set offline assigned distribution set Signed-off-by: Florian Bezannier <florian.bezannier@hotmail.fr> * refacto: apply @avgustinmm recommendation * docs: Mark update offline API as experimental --------- Signed-off-by: Florian Bezannier <florian.bezannier@hotmail.fr>
This commit is contained in:
committed by
GitHub
parent
aa8ab69c1f
commit
0013750f78
@@ -527,4 +527,22 @@ public interface ControllerManagement {
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
||||
void deactivateAutoConfirmation(@NotEmpty String controllerId);
|
||||
|
||||
/**
|
||||
* Updates distributionSet installed version (experimental)
|
||||
*
|
||||
* @param distributionName
|
||||
* installed
|
||||
* @param version
|
||||
* installed
|
||||
*
|
||||
* @return updated {@link Target}
|
||||
*
|
||||
* @throws EntityNotFoundException
|
||||
* if target that has to be updated could not be found
|
||||
* @throws java.util.NoSuchElementException
|
||||
* if DistributionSetAssignmentResult list is empty
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
|
||||
boolean updateOfflineAssignedVersion(@NotEmpty String controllerId, String distributionName, String version);
|
||||
}
|
||||
|
||||
@@ -165,6 +165,9 @@ public interface DeploymentManagement {
|
||||
* target and multiassignment is disabled
|
||||
*/
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET)
|
||||
List<DistributionSetAssignmentResult> offlineAssignedDistributionSets(Collection<Entry<String, Long>> assignments, String initiatedBy);
|
||||
|
||||
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY)
|
||||
List<DistributionSetAssignmentResult> offlineAssignedDistributionSets(Collection<Entry<String, Long>> assignments);
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ import static org.eclipse.hawkbit.repository.model.Action.Status.DOWNLOADED;
|
||||
import static org.eclipse.hawkbit.repository.model.Action.Status.FINISHED;
|
||||
import static org.eclipse.hawkbit.repository.model.Target.CONTROLLER_ATTRIBUTE_KEY_SIZE;
|
||||
import static org.eclipse.hawkbit.repository.model.Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE;
|
||||
import static org.eclipse.hawkbit.security.SecurityContextTenantAware.SYSTEM_USER;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
@@ -44,11 +45,14 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.ListUtils;
|
||||
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||
import org.eclipse.hawkbit.repository.EntityFactory;
|
||||
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
|
||||
import org.eclipse.hawkbit.repository.QuotaManagement;
|
||||
import org.eclipse.hawkbit.repository.RepositoryConstants;
|
||||
import org.eclipse.hawkbit.repository.RepositoryProperties;
|
||||
import org.eclipse.hawkbit.repository.TargetTypeManagement;
|
||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetTypeManagement;
|
||||
import org.eclipse.hawkbit.repository.UpdateMode;
|
||||
@@ -85,6 +89,7 @@ import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
@@ -161,6 +166,12 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
||||
@Autowired
|
||||
private TargetTypeManagement targetTypeManagement;
|
||||
|
||||
@Autowired
|
||||
private DeploymentManagement deploymentManagement;
|
||||
|
||||
@Autowired
|
||||
private DistributionSetManagement distributionSetManagement;
|
||||
|
||||
public JpaControllerManagement(final ScheduledExecutorService executorService,
|
||||
final ActionRepository actionRepository, final ActionStatusRepository actionStatusRepository,
|
||||
final QuotaManagement quotaManagement, final RepositoryProperties repositoryProperties) {
|
||||
@@ -1095,6 +1106,22 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
||||
confirmationManagement.deactivateAutoConfirmation(controllerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateOfflineAssignedVersion(@NotEmpty String controllerId, String distributionName, String version){
|
||||
List<DistributionSetAssignmentResult> distributionSetAssignmentResults =
|
||||
systemSecurityContext.runAsSystem(() ->
|
||||
distributionSetManagement.getByNameAndVersion(distributionName,version).map(
|
||||
distributionSet -> deploymentManagement.offlineAssignedDistributionSets(
|
||||
List.of(Map.entry(controllerId, distributionSet.getId())),controllerId))
|
||||
.orElseThrow(() ->
|
||||
new EntityNotFoundException(DistributionSet.class, Map.entry(distributionName, version)))
|
||||
.stream().toList());
|
||||
boolean notAlreadyAssigned = distributionSetAssignmentResults.stream().findFirst()
|
||||
.map(result-> result.getAlreadyAssigned()==0)
|
||||
.orElseThrow();
|
||||
return notAlreadyAssigned;
|
||||
}
|
||||
|
||||
private void cancelAssignDistributionSetEvent(final Action action) {
|
||||
afterCommit.afterCommit(() -> eventPublisherHolder.getEventPublisher()
|
||||
.publishEvent(new CancelTargetAssignmentEvent(action, eventPublisherHolder.getApplicationId())));
|
||||
|
||||
@@ -185,6 +185,12 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
||||
@Transactional(isolation = Isolation.READ_COMMITTED)
|
||||
public List<DistributionSetAssignmentResult> offlineAssignedDistributionSets(
|
||||
final Collection<Entry<String, Long>> assignments) {
|
||||
return offlineAssignedDistributionSets(assignments,tenantAware.getCurrentUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DistributionSetAssignmentResult> offlineAssignedDistributionSets(
|
||||
Collection<Entry<String, Long>> assignments, String initiatedBy) {
|
||||
final Collection<Entry<String, Long>> distinctAssignments = assignments.stream().distinct().toList();
|
||||
enforceMaxAssignmentsPerRequest(distinctAssignments.size());
|
||||
|
||||
|
||||
@@ -1564,6 +1564,36 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
|
||||
assertThat(foundAction).isPresent();
|
||||
assertThat(foundAction.get().getId()).isEqualTo(actionId);
|
||||
}
|
||||
@Test
|
||||
@Description("Verify that assigning version form target works")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3)}
|
||||
)
|
||||
void assignVersionToTarget() {
|
||||
|
||||
final DistributionSet knownDistributionSet = testdataFactory.createDistributionSet();
|
||||
|
||||
// GIVEN
|
||||
testdataFactory.createTarget(DEFAULT_CONTROLLER_ID).getId();
|
||||
|
||||
// WHEN
|
||||
boolean updated1 = controllerManagement.updateOfflineAssignedVersion(DEFAULT_CONTROLLER_ID,
|
||||
knownDistributionSet.getName(),knownDistributionSet.getVersion());
|
||||
// if target is already assigned to a distribution then it shouldn't reassign the distribution
|
||||
boolean updated2 = controllerManagement.updateOfflineAssignedVersion(DEFAULT_CONTROLLER_ID,
|
||||
knownDistributionSet.getName(),knownDistributionSet.getVersion());
|
||||
|
||||
// THEN
|
||||
assertAssignedDistributionSetId(DEFAULT_CONTROLLER_ID, knownDistributionSet.getId());
|
||||
assertInstalledDistributionSetId(DEFAULT_CONTROLLER_ID, knownDistributionSet.getId());
|
||||
assertThat(updated1).isTrue();
|
||||
assertThat(updated2).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that a null externalRef cannot be assigned to an action")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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.ddi.json.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/**
|
||||
* Allow a target to declare running distribution set version
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiAssignedVersion {
|
||||
|
||||
@Schema(description = "Distribution Set name", example = "linux")
|
||||
private final String name;
|
||||
|
||||
@Schema(description = "Distribution set version", example = "1.2.3")
|
||||
private final String version;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param name
|
||||
* Distribution set name
|
||||
* @param version
|
||||
* Distribution set version
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiAssignedVersion(@JsonProperty(value = "name", required = true) String name,
|
||||
@JsonProperty(value = "version", required = true) String version) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DdiInstalledVersion{" + "name='" + name + '\'' + ", version='" + version + '\'' + '}';
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBaseAction;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeploymentBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAssignedVersion;
|
||||
import org.eclipse.hawkbit.rest.json.model.ExceptionInfo;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -826,4 +827,40 @@ public interface DdiRootControllerRestApi {
|
||||
+ DdiRestConstants.CONFIRMATION_BASE + "/" + DdiRestConstants.AUTO_CONFIRM_DEACTIVATE)
|
||||
ResponseEntity<Void> deactivateAutoConfirmation(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assign an already installed distribution for a target
|
||||
*
|
||||
* @param tenant
|
||||
* of the client
|
||||
* to provide
|
||||
* @param controllerId
|
||||
* of the target that matches to controller id
|
||||
* @param ddiAssignedVersion
|
||||
* as {@link DdiAssignedVersion}
|
||||
*
|
||||
* @return the response
|
||||
*/
|
||||
@Operation(summary = "Set offline assigned version", description = """
|
||||
Allow to set current running version.
|
||||
""")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Successfully retrieved"),
|
||||
@ApiResponse(responseCode = "400", description = "Bad Request - e.g. invalid parameters", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionInfo.class))),
|
||||
@ApiResponse(responseCode = "401", description = "The request requires user authentication.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions, entity is not allowed to be changed (i.e. read-only) or data volume restriction applies.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
|
||||
@ApiResponse(responseCode = "404", description = "Target or Distribution not found", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
|
||||
@ApiResponse(responseCode = "405", description = "The http request method is not allowed on the resource.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
|
||||
@ApiResponse(responseCode = "406", description = "In case accept header is specified and not application/json.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
|
||||
@ApiResponse(responseCode = "409", description = "E.g. in case an entity is created or modified by another user in another request at the same time. You may retry your modification request.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
|
||||
@ApiResponse(responseCode = "410", description = "Action is not active anymore.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
|
||||
@ApiResponse(responseCode = "415", description = "The request was attempt with a media-type which is not supported by the server for this resource.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true))),
|
||||
@ApiResponse(responseCode = "429", description = "Too many requests. The server will refuse further attempts and the client has to wait another second.", content = @Content(mediaType = "application/json", schema = @Schema(hidden = true)))
|
||||
})
|
||||
@PostMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
|
||||
+ DdiRestConstants.INSTALLED_BASE_ACTION, consumes = {
|
||||
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<Void> setAsssignedOfflineVersion(@Valid DdiAssignedVersion ddiAssignedVersion,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.eclipse.hawkbit.ddi.json.model.DdiDeployment;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiDeploymentBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAssignedVersion;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult.FinalResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiUpdateMode;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
@@ -83,7 +84,6 @@ import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
@@ -757,4 +757,14 @@ public class DdiRootController implements DdiRootControllerRestApi {
|
||||
confirmationManagement.deactivateAutoConfirmation(controllerId);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> setAsssignedOfflineVersion(@Valid @RequestBody DdiAssignedVersion ddiAssignedVersion,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId){
|
||||
boolean updated = controllerManagement.updateOfflineAssignedVersion(controllerId,
|
||||
ddiAssignedVersion.getName(), ddiAssignedVersion.getVersion());
|
||||
if (updated)
|
||||
return new ResponseEntity<>(HttpStatus.CREATED);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.List;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAssignedVersion;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiProgress;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
@@ -71,6 +72,8 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
|
||||
protected static final String DEPLOYMENT_BASE = CONTROLLER_BASE + "/deploymentBase/{actionId}";
|
||||
protected static final String CANCEL_ACTION = CONTROLLER_BASE + "/cancelAction/{actionId}";
|
||||
protected static final String INSTALLED_BASE = CONTROLLER_BASE + "/installedBase/{actionId}";
|
||||
protected static final String INSTALLED_BASE_ROOT = CONTROLLER_BASE + "/installedBase";
|
||||
|
||||
protected static final String DEPLOYMENT_FEEDBACK = DEPLOYMENT_BASE + "/feedback";
|
||||
protected static final String CANCEL_FEEDBACK = CANCEL_ACTION + "/feedback";
|
||||
|
||||
@@ -134,6 +137,13 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
|
||||
statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions postInstalledBase(final String controllerId, final String content,
|
||||
final ResultMatcher statusMatcher) throws Exception {
|
||||
return mvc.perform(post(INSTALLED_BASE_ROOT, tenantAware.getCurrentTenant(), controllerId)
|
||||
.content(content.getBytes()).contentType(MediaType.APPLICATION_JSON_UTF8))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions postDeploymentFeedback(final MediaType mediaType, final String controllerId,
|
||||
final Long actionId, final byte[] content, final ResultMatcher statusMatcher) throws Exception {
|
||||
return mvc
|
||||
@@ -360,6 +370,10 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat
|
||||
return objectMapper.writeValueAsString(new DdiConfirmationFeedback(confirmation, code, messages));
|
||||
}
|
||||
|
||||
protected String getJsonInstalledBase(String name, String version) throws JsonProcessingException {
|
||||
return objectMapper.writeValueAsString(new DdiAssignedVersion(name, version));
|
||||
}
|
||||
|
||||
protected static ObjectMapper getMapper(){
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
@@ -102,6 +102,37 @@ public class DdiInstalledBaseTest extends AbstractDDiApiIntegrationTest {
|
||||
String.valueOf(softwareModuleId));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that assigned version is self assigned version")
|
||||
public void installedVersion() throws Exception {
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
|
||||
// update assigned version
|
||||
postInstalledBase(target.getControllerId(),getJsonInstalledBase(ds.getName(),ds.getVersion()),status()
|
||||
.isCreated());
|
||||
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).get().getId())
|
||||
.isEqualTo(ds.getId());
|
||||
|
||||
// update assigned version while version already assigned
|
||||
postInstalledBase(target.getControllerId(),getJsonInstalledBase(ds.getName(),ds.getVersion()),status().isOk());
|
||||
}
|
||||
@Test
|
||||
@Description("Ensure that installedVersion is version self assigned")
|
||||
public void installedVersionNotExist() throws Exception {
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final String dsName = "unknown";
|
||||
final String dsVersion = "1.0.0";
|
||||
|
||||
|
||||
// get installed base
|
||||
postInstalledBase(target.getControllerId(),getJsonInstalledBase(dsName,dsVersion),status().isNotFound());
|
||||
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(target.getControllerId()).isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test several deployments to a controller. Checks that action is represented as installedBase after installation.")
|
||||
public void deploymentSeveralActionsInInstalledBase() throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user