Moves DDI artifacts into a dedicated directory/artifact parent (#2002)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
2
hawkbit-ddi/hawkbit-boot-starter-ddi-api/README.md
Normal file
2
hawkbit-ddi/hawkbit-boot-starter-ddi-api/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
[Spring Boot Starter](http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-starter) for
|
||||
the [Direct Device Integration API](https://www.eclipse.org/hawkbit/documentation/interfaces/ddi-api.html).
|
||||
76
hawkbit-ddi/hawkbit-boot-starter-ddi-api/pom.xml
Normal file
76
hawkbit-ddi/hawkbit-boot-starter-ddi-api/pom.xml
Normal file
@@ -0,0 +1,76 @@
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-ddi-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-boot-starter-ddi-api</artifactId>
|
||||
<name>hawkBit :: Spring Boot Starter DDI API</name>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring - START -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-config</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-aspects</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring - END -->
|
||||
|
||||
<!-- hawkBit - START -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-ddi-resource</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-security-integration</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-http-security</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-autoconfigure</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<!-- hawkBit - END -->
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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.autoconfigure.ddi;
|
||||
|
||||
import org.eclipse.hawkbit.ddi.rest.resource.DdiApiConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* Auto-Configuration for enabling the DDI REST-Resources.
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(DdiApiConfiguration.class)
|
||||
@Import(DdiApiConfiguration.class)
|
||||
public class DDiApiAutoConfiguration {
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.eclipse.hawkbit.autoconfigure.ddi.DDiApiAutoConfiguration
|
||||
18
hawkbit-ddi/hawkbit-ddi-api/README.md
Normal file
18
hawkbit-ddi/hawkbit-ddi-api/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Eclipse.IoT hawkBit - Direct Device Integration API - Model and Resources
|
||||
|
||||
The Direct Device Integration (DDI) API is used by devices for communicating with the HawkBit Update Server through
|
||||
HTTP.
|
||||
|
||||
# Version 1
|
||||
|
||||
The model follows [semantic versioning](http://semver.org) with MAJOR version, i.e. breaking changes will result in a
|
||||
new MAJOR version.
|
||||
|
||||
# Compile
|
||||
|
||||
#### Build hawkbit-ddi-api
|
||||
|
||||
```
|
||||
$ cd hawkbit/hawkbit-ddi-api
|
||||
$ mvn clean install
|
||||
```
|
||||
65
hawkbit-ddi/hawkbit-ddi-api/pom.xml
Normal file
65
hawkbit-ddi/hawkbit-ddi-api/pom.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-ddi-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-ddi-api</artifactId>
|
||||
<name>hawkBit :: REST :: DDI API</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.hateoas</groupId>
|
||||
<artifactId>spring-hateoas</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-cbor</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-rest-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- TEST -->
|
||||
<dependency>
|
||||
<groupId>io.github.classgraph</groupId>
|
||||
<artifactId>classgraph</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
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;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* After the HawkBit Target has executed an action, received by a GET(URL)
|
||||
* request it reports the completion of it to the HawkBit Server with a action
|
||||
* status message, i.e. with a PUT message to the feedback channel, i.e. PUT
|
||||
* URL/feedback. This message could be used not only at the end of execution but
|
||||
* also as status updates during a longer lasting execution period. The format
|
||||
* of each action answer message is defined below at each action. But it is
|
||||
* expected, that the contents of the message answers have all a similar
|
||||
* structure: The content starts with a generic header and additional elements.
|
||||
* *
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The answer header would look like: { "time": "20140511T121314", "status": {
|
||||
* "execution": "closed", "result": { "final": "success", "progress": {} }
|
||||
* "details": [], } }
|
||||
* </p>
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiActionFeedback {
|
||||
|
||||
@Schema(description = "Timestamp of the action", example = "2023-08-03T12:31:41.890992967Z")
|
||||
private final String time;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
private final DdiStatus status;
|
||||
|
||||
/**
|
||||
* Constructs an action-feedback
|
||||
*
|
||||
* @param time time of feedback
|
||||
* @param status status to be appended to the action
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiActionFeedback(
|
||||
@JsonProperty(value = "time") final String time,
|
||||
@JsonProperty(value = "status", required = true) final DdiStatus status) {
|
||||
this.time = time;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2017 Siemens AG
|
||||
*
|
||||
* 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 java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi;
|
||||
|
||||
/**
|
||||
* Provide action history information to the controller as part of response to
|
||||
* {@link DdiRootControllerRestApi#getControllerDeploymentBaseAction} and
|
||||
* {@link DdiRootControllerRestApi#getConfirmationBaseAction}:
|
||||
* <ol>
|
||||
* <li>Current action status at the server</li>
|
||||
* <li>List of messages from action history</li>
|
||||
* </ol>
|
||||
* that were sent to server earlier by the controller using {@link DdiActionFeedback}.
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonPropertyOrder({ "status", "messages" })
|
||||
public class DdiActionHistory {
|
||||
|
||||
@JsonProperty("status")
|
||||
@Schema(description = "Status of the deployment based on previous feedback by the device", example = "RUNNING")
|
||||
private final String actionStatus;
|
||||
|
||||
@JsonProperty("messages")
|
||||
@Schema(description = "Messages are previously sent to the feedback channel in LIFO order by the device. Note: The first status message is set by the system and describes the trigger of the deployment")
|
||||
private final List<String> messages;
|
||||
|
||||
/**
|
||||
* Parameterized constructor for creating {@link DdiActionHistory}.
|
||||
*
|
||||
* @param actionStatus is the current action status at the server
|
||||
* @param messages is a list of messages retrieved from action history.
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiActionHistory(
|
||||
@JsonProperty("status") final String actionStatus,
|
||||
@JsonProperty("messages") List<String> messages) {
|
||||
this.actionStatus = actionStatus;
|
||||
this.messages = messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO 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;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiActivateAutoConfirmation {
|
||||
|
||||
@JsonProperty(required = false)
|
||||
@Schema(description = "Individual value (e.g. username) stored as initiator and automatically used as confirmed" +
|
||||
" user in future actions", example = "exampleUser")
|
||||
private final String initiator;
|
||||
|
||||
@JsonProperty(required = false)
|
||||
@Schema(description = "Individual value to attach a remark which will be persisted when automatically " +
|
||||
"confirming future actions", example = "exampleRemark")
|
||||
private final String remark;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param initiator can be null
|
||||
* @param remark can be null
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiActivateAutoConfirmation(@JsonProperty(value = "initiator") final String initiator,
|
||||
@JsonProperty(value = "remark") final String remark) {
|
||||
this.initiator = initiator;
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
|
||||
/**
|
||||
* Download information for all artifacts related to a specific {@link DdiChunk}
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(description = """
|
||||
**_links**:
|
||||
* **download** - HTTPs Download resource for artifacts. The resource supports partial download as specified by RFC7233 (range requests). Keep in mind that the target needs to have the artifact assigned in order to be granted permission to download.
|
||||
* **md5sum** - HTTPs Download resource for MD5SUM file is an optional auto generated artifact that is especially useful for Linux based devices on order to check artifact consistency after download by using the md5sum command line tool. The MD5 and SHA1 are in addition available as metadata in the deployment command itself.
|
||||
* **download-http** - HTTP Download resource for artifacts. The resource supports partial download as specified by RFC7233 (range requests). Keep in mind that the target needs to have the artifact assigned in order to be granted permission to download. (note: anonymous download needs to be enabled on the service account for non-TLS access)
|
||||
* **md5sum-http** - HTTP Download resource for MD5SUM file is an optional auto generated artifact that is especially useful for Linux based devices on order to check artifact consistency after download by using the md5sum command line tool. The MD5 and SHA1 are in addition available as metadata in the deployment command itself. (note: anonymous download needs to be enabled on the service account for non-TLS access)
|
||||
""", example = """
|
||||
{
|
||||
"filename" : "binaryFile",
|
||||
"hashes" : {
|
||||
"sha1" : "e4e667b70ff652cb9d9c8a49f141bd68e06cec6f",
|
||||
"md5" : "13793b0e3a7830ed685d3ede7ff93048",
|
||||
"sha256" : "c51368bf045803b429a67bdf04539a373d9fb8caa310fe0431265e6871b4f07a"
|
||||
},
|
||||
"size" : 11,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/40/filename/binaryFile.MD5SUM"
|
||||
}
|
||||
}
|
||||
}""")
|
||||
public class DdiArtifact extends RepresentationModel<DdiArtifact> {
|
||||
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@Schema(description = "File name", example = "binary.tgz")
|
||||
private String filename;
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description = "Artifact hashes")
|
||||
private DdiArtifactHash hashes;
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description = "Artifact size", example = "3")
|
||||
private Long size;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Hashes for given Artifact
|
||||
*/
|
||||
@NoArgsConstructor // needed for json create
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiArtifactHash {
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description = "SHA1 hash of the artifact in Base 16 format", example = "2d86c2a659e364e9abba49ea6ffcd53dd5559f05")
|
||||
private String sha1;
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description = "MD5 hash of the artifact", example = "0d1b08c34858921bc7c662b228acb7ba")
|
||||
private String md5;
|
||||
|
||||
@JsonProperty
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@Schema(description = "SHA-256 hash of the artifact in Base 16 format", example = "a03b221c6c6eae7122ca51695d456d5222e524889136394944b2f9763b483615")
|
||||
private String sha256;
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*
|
||||
* @param sha1 sha1 hash of the artifact
|
||||
* @param md5 md5 hash of the artifact
|
||||
* @param sha256 sha256 hash of the artifact
|
||||
*/
|
||||
public DdiArtifactHash(final String sha1, final String md5, final String sha256) {
|
||||
this.sha1 = sha1;
|
||||
this.md5 = md5;
|
||||
this.sha256 = sha256;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 + '\'' + '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO 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 jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
|
||||
@NoArgsConstructor // needed for json create
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonPropertyOrder({ "active", "initiator", "remark", "activatedAt" })
|
||||
public class DdiAutoConfirmationState extends RepresentationModel<DdiAutoConfirmationState> {
|
||||
|
||||
@NotNull
|
||||
@Schema(example = "true")
|
||||
private boolean active;
|
||||
@Schema(example = "exampleUserId")
|
||||
private String initiator;
|
||||
@Schema(example = "exampleRemark")
|
||||
private String remark;
|
||||
|
||||
@Schema(example = "1691065895439")
|
||||
private Long activatedAt;
|
||||
|
||||
public static DdiAutoConfirmationState active(final long activatedAt) {
|
||||
final DdiAutoConfirmationState state = new DdiAutoConfirmationState();
|
||||
state.setActive(true);
|
||||
state.setActivatedAt(activatedAt);
|
||||
return state;
|
||||
}
|
||||
|
||||
public static DdiAutoConfirmationState disabled() {
|
||||
return new DdiAutoConfirmationState();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 jakarta.validation.constraints.NotNull;
|
||||
|
||||
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;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Cancel action to be provided to the target.
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiCancel {
|
||||
|
||||
@Schema(description = "Id of the action", example = "11")
|
||||
private final String id;
|
||||
|
||||
@Schema(description = "Action that needs to be canceled")
|
||||
@NotNull
|
||||
private final DdiCancelActionToStop cancelAction;
|
||||
|
||||
/**
|
||||
* Parameterized constructor.
|
||||
*
|
||||
* @param id of the cancel action
|
||||
* @param cancelAction the action
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiCancel(@JsonProperty("id") final String id,
|
||||
@JsonProperty("cancelAction") final DdiCancelActionToStop cancelAction) {
|
||||
this.id = id;
|
||||
this.cancelAction = cancelAction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 jakarta.validation.constraints.NotNull;
|
||||
|
||||
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;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* The action that has to be stopped by the target.
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiCancelActionToStop {
|
||||
|
||||
@NotNull
|
||||
@Schema(description = "Id of the action that needs to be canceled (typically identical to id field on the cancel action itself)", example = "11")
|
||||
private final String stopId;
|
||||
|
||||
/**
|
||||
* Parameterized constructor.
|
||||
*
|
||||
* @param stopId ID of the action to be stopped
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiCancelActionToStop(@JsonProperty("stopId") final String stopId) {
|
||||
this.stopId = stopId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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 java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Deployment chunks.
|
||||
*/
|
||||
@NoArgsConstructor // needed for json create
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiChunk {
|
||||
|
||||
@JsonProperty("part")
|
||||
@NotNull
|
||||
@Schema(description = "Type of the chunk, e.g. firmware, bundle, app. In update server mapped to Software Module Type")
|
||||
private String part;
|
||||
|
||||
@JsonProperty("version")
|
||||
@NotNull
|
||||
@Schema(description = "Software version of the chunk", example = "1.2.0")
|
||||
private String version;
|
||||
|
||||
@JsonProperty("name")
|
||||
@NotNull
|
||||
@Schema(description = "Name of the chunk")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("encrypted")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = "If encrypted")
|
||||
private Boolean encrypted;
|
||||
|
||||
@JsonProperty("artifacts")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = "List of artifacts")
|
||||
private List<DdiArtifact> artifacts;
|
||||
|
||||
@JsonProperty("metadata")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = "Meta data of the respective software module that has been marked with 'target visible'")
|
||||
private List<DdiMetadata> metadata;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param part of the deployment chunk
|
||||
* @param version of the artifact
|
||||
* @param name of the artifact
|
||||
* @param encrypted if artifacts are encrypted
|
||||
* @param artifacts download information
|
||||
* @param metadata optional as additional information for the target/device
|
||||
*/
|
||||
public DdiChunk(final String part, final String version, final String name, final Boolean encrypted,
|
||||
final List<DdiArtifact> artifacts, final List<DdiMetadata> metadata) {
|
||||
this.part = part;
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.encrypted = encrypted;
|
||||
this.artifacts = artifacts;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public List<DdiArtifact> getArtifacts() {
|
||||
if (artifacts == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(artifacts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Standard configuration for the target.
|
||||
*/
|
||||
@NoArgsConstructor // needed for json deserialization
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(description = "DDI controller configuration")
|
||||
public class DdiConfig {
|
||||
|
||||
@JsonProperty
|
||||
private DdiPolling polling;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param polling configuration of the polling for the target
|
||||
*/
|
||||
public DdiConfig(final DdiPolling polling) {
|
||||
this.polling = polling;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 java.util.Map;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
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;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Feedback channel for ConfigData action.
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(example = """
|
||||
{
|
||||
"mode" : "merge",
|
||||
"data" : {
|
||||
"VIN" : "JH4TB2H26CC000000",
|
||||
"hwRevision" : "2"
|
||||
}
|
||||
}""")
|
||||
public class DdiConfigData {
|
||||
|
||||
@NotEmpty
|
||||
@Schema(description = "Link which is provided whenever the provisioning target or device is supposed to push its configuration data (aka. \"controller attributes\") to the server. Only shown for the initial configuration, after a successful update action, or if requested explicitly (e.g. via the management UI).")
|
||||
private final Map<String, String> data;
|
||||
|
||||
@Schema(description = "Optional parameter to specify the update mode that should be applied when updating target attributes. Valid values are 'merge', 'replace', and 'remove'. Defaults to 'merge'.")
|
||||
private final DdiUpdateMode mode;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param data contains the attributes.
|
||||
* @param mode defines the mode of the update (replace, merge, remove)
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiConfigData(
|
||||
@JsonProperty(value = "data") final Map<String, String> data,
|
||||
@JsonProperty(value = "mode") final DdiUpdateMode mode) {
|
||||
this.data = data;
|
||||
this.mode = mode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO 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 jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
|
||||
/**
|
||||
* Confirmation base response.
|
||||
* Set order to place links at last.
|
||||
*/
|
||||
@NoArgsConstructor // needed for json create
|
||||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonPropertyOrder({ "autoConfirm" })
|
||||
@Schema(
|
||||
description = """
|
||||
**_links**:
|
||||
* **confirmationBase** - confirmation base
|
||||
* **deactivateAutoConfirm** - where to deactivate auto confirm
|
||||
* **activateAutoConfirm** - where to activate auto confirm
|
||||
""",
|
||||
example = """
|
||||
{
|
||||
"autoConfirm" : {
|
||||
"active" : false
|
||||
},
|
||||
"_links" : {
|
||||
"activateAutoConfirm" : {
|
||||
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/confirmationBase/activateAutoConfirm"
|
||||
},
|
||||
"confirmationBase" : {
|
||||
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/confirmationBase/10?c=-2122565939"
|
||||
}
|
||||
}
|
||||
}""")
|
||||
public class DdiConfirmationBase extends RepresentationModel<DdiConfirmationBase> {
|
||||
|
||||
@JsonProperty("autoConfirm")
|
||||
@NotNull
|
||||
private DdiAutoConfirmationState autoConfirm;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param autoConfirmState
|
||||
*/
|
||||
public DdiConfirmationBase(final DdiAutoConfirmationState autoConfirmState) {
|
||||
this.autoConfirm = autoConfirmState;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO 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 jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
|
||||
/**
|
||||
* Update action resource.
|
||||
*/
|
||||
@NoArgsConstructor // needed for json create
|
||||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonPropertyOrder({ "id", "confirmation", "actionHistory" })
|
||||
@Schema(description = """
|
||||
The response body includes the detailed information about the action awaiting confirmation in the same format as
|
||||
for the deploymentBase operation.""",
|
||||
example = """
|
||||
{
|
||||
"id" : "6",
|
||||
"confirmation" : {
|
||||
"download" : "forced",
|
||||
"update" : "forced",
|
||||
"maintenanceWindow" : "available",
|
||||
"chunks" : [ {
|
||||
"part" : "jvm",
|
||||
"version" : "1.0.62",
|
||||
"name" : "oneapp runtime",
|
||||
"artifacts" : [ {
|
||||
"filename" : "binary.tgz",
|
||||
"hashes" : {
|
||||
"sha1" : "3dceccec02e7626184bdbba12b247b67ff04c363",
|
||||
"md5" : "a9a7df0aa4c72b3b03b654c42d29744b",
|
||||
"sha256" : "971d8db88fef8e7a3e6d5bbf501d69b07d0c300d9be948aff8b52960ef039358"
|
||||
},
|
||||
"size" : 11,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/binary.tgz.MD5SUM"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"filename" : "file.signature",
|
||||
"hashes" : {
|
||||
"sha1" : "3dceccec02e7626184bdbba12b247b67ff04c363",
|
||||
"md5" : "a9a7df0aa4c72b3b03b654c42d29744b",
|
||||
"sha256" : "971d8db88fef8e7a3e6d5bbf501d69b07d0c300d9be948aff8b52960ef039358"
|
||||
},
|
||||
"size" : 11,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/17/filename/file.signature.MD5SUM"
|
||||
}
|
||||
}
|
||||
} ]
|
||||
}, {
|
||||
"part" : "bApp",
|
||||
"version" : "1.0.96",
|
||||
"name" : "oneapplication",
|
||||
"artifacts" : [ {
|
||||
"filename" : "binary.tgz",
|
||||
"hashes" : {
|
||||
"sha1" : "701c0c0fcbee5e96fa5c5b819cb519686940ade3",
|
||||
"md5" : "f0f6a34c4c9e79d07c2d92c3c3d88560",
|
||||
"sha256" : "cff472a07c3143741fb03ac6c577acabef72a186a8bfaab00bbb47ca5ebbe554"
|
||||
},
|
||||
"size" : 11,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/binary.tgz.MD5SUM"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"filename" : "file.signature",
|
||||
"hashes" : {
|
||||
"sha1" : "701c0c0fcbee5e96fa5c5b819cb519686940ade3",
|
||||
"md5" : "f0f6a34c4c9e79d07c2d92c3c3d88560",
|
||||
"sha256" : "cff472a07c3143741fb03ac6c577acabef72a186a8bfaab00bbb47ca5ebbe554"
|
||||
},
|
||||
"size" : 11,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/16/filename/file.signature.MD5SUM"
|
||||
}
|
||||
}
|
||||
} ]
|
||||
}, {
|
||||
"part" : "os",
|
||||
"version" : "1.0.44",
|
||||
"name" : "one Firmware",
|
||||
"artifacts" : [ {
|
||||
"filename" : "binary.tgz",
|
||||
"hashes" : {
|
||||
"sha1" : "2b09765e953cd138b7da8f4725e48183dab62aec",
|
||||
"md5" : "9b0aa2f51379cb4a5e0b7d026c2605c9",
|
||||
"sha256" : "618faa741070b3f8148bad06f088e537a8f7913e734df4dde61fb163725cb4ee"
|
||||
},
|
||||
"size" : 15,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/binary.tgz.MD5SUM"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"filename" : "file.signature",
|
||||
"hashes" : {
|
||||
"sha1" : "2b09765e953cd138b7da8f4725e48183dab62aec",
|
||||
"md5" : "9b0aa2f51379cb4a5e0b7d026c2605c9",
|
||||
"sha256" : "618faa741070b3f8148bad06f088e537a8f7913e734df4dde61fb163725cb4ee"
|
||||
},
|
||||
"size" : 15,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/18/filename/file.signature.MD5SUM"
|
||||
}
|
||||
}
|
||||
} ],
|
||||
"metadata" : [ {
|
||||
"key" : "aMetadataKey",
|
||||
"value" : "Metadata value as defined in software module"
|
||||
} ]
|
||||
} ]
|
||||
},
|
||||
"actionHistory" : {
|
||||
"status" : "WAIT_FOR_CONFIRMATION",
|
||||
"messages" : [ "Assignment initiated by user 'TestPrincipal'", "Waiting for the confirmation by the device before processing with the deployment" ]
|
||||
}
|
||||
}""")
|
||||
public class DdiConfirmationBaseAction extends RepresentationModel<DdiConfirmationBaseAction> {
|
||||
|
||||
@JsonProperty("id")
|
||||
@NotNull
|
||||
@Schema(description = "Id of the action", example = "6")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("confirmation")
|
||||
@NotNull
|
||||
@Schema(description = "Deployment confirmation operation")
|
||||
private DdiDeployment confirmation;
|
||||
|
||||
/**
|
||||
* Action history containing current action status and a list of feedback
|
||||
* messages received earlier from the controller.
|
||||
*/
|
||||
@JsonProperty("actionHistory")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = """
|
||||
(Optional) GET parameter to retrieve a given number of messages which are previously
|
||||
provided by the device. Useful if the devices sent state information to the feedback channel and never
|
||||
stored them locally""")
|
||||
private DdiActionHistory actionHistory;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param id of the update action
|
||||
* @param confirmation chunk details
|
||||
* @param actionHistory containing current action status and a list of feedback messages
|
||||
* received earlier from the controller.
|
||||
*/
|
||||
public DdiConfirmationBaseAction(final String id, final DdiDeployment confirmation,
|
||||
final DdiActionHistory actionHistory) {
|
||||
this.id = id;
|
||||
this.confirmation = confirmation;
|
||||
this.actionHistory = actionHistory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO 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 java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* New update actions require confirmation when confirmation flow is switched on. This is the feedback channel for
|
||||
* confirmation messages for DDI API. The confirmation message has a mandatory field confirmation with possible values:
|
||||
* "confirmed" and "denied".
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiConfirmationFeedback {
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(description = "Action confirmation state")
|
||||
private final Confirmation confirmation;
|
||||
@Schema(description = "(Optional) Individual status code", example = "200")
|
||||
private final Integer code;
|
||||
@Schema(description = "List of detailed message information", example = "[ \"Feedback message\" ]")
|
||||
private final List<String> details;
|
||||
|
||||
/**
|
||||
* Constructs an confirmation-feedback
|
||||
*
|
||||
* @param confirmation confirmation value for the action. Valid values are "Confirmed" and "Denied
|
||||
* @param code code for confirmation
|
||||
* @param details messages
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiConfirmationFeedback(
|
||||
@JsonProperty(value = "confirmation", required = true) final Confirmation confirmation,
|
||||
@JsonProperty(value = "code") final Integer code,
|
||||
@JsonProperty(value = "details") final List<String> details) {
|
||||
this.confirmation = confirmation;
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public List<String> getDetails() {
|
||||
if (details == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.unmodifiableList(details);
|
||||
}
|
||||
|
||||
public enum Confirmation {
|
||||
/**
|
||||
* Confirm the action.
|
||||
*/
|
||||
CONFIRMED("confirmed"),
|
||||
|
||||
/**
|
||||
* Deny the action.
|
||||
*/
|
||||
DENIED("denied");
|
||||
|
||||
private final String name;
|
||||
|
||||
Confirmation(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
|
||||
/**
|
||||
* {@link DdiControllerBase} resource content.
|
||||
*/
|
||||
@NoArgsConstructor // needed for json deserialization
|
||||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(description = """
|
||||
**_links**: Actions that the server has for the target
|
||||
* **deploymentBase** - Detailed deployment operation
|
||||
* **installedBase** - Detailed operation of last successfully finished action
|
||||
* **configData** - Link which is provided whenever the provisioning target or device is supposed to push its configuration data (aka. "controller attributes") to the server. Only shown for the initial configuration, after a successful update action, or if requested explicitly (e.g. via the management UI)
|
||||
""",
|
||||
example = """
|
||||
{
|
||||
"config" : {
|
||||
"polling" : {
|
||||
"sleep" : "12:00:00"
|
||||
}
|
||||
},
|
||||
"_links" : {
|
||||
"deploymentBase" : {
|
||||
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/deploymentBase/5?c=-2127183556"
|
||||
},
|
||||
"installedBase" : {
|
||||
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/installedBase/4"
|
||||
},
|
||||
"configData" : {
|
||||
"href" : "https://management-api.host.com/TENANT_ID/controller/v1/CONTROLLER_ID/configData"
|
||||
}
|
||||
}
|
||||
}""")
|
||||
public class DdiControllerBase extends RepresentationModel<DdiControllerBase> {
|
||||
|
||||
@JsonProperty
|
||||
private DdiConfig config;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param config configuration of the SP target
|
||||
*/
|
||||
public DdiControllerBase(final DdiConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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 java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Detailed update action information.
|
||||
*/
|
||||
@NoArgsConstructor // needed for json create
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiDeployment {
|
||||
|
||||
@Schema(description = """
|
||||
Handling for the download part of the provisioning process ('skip': do not download yet, 'attempt': server asks
|
||||
to download, 'forced': server requests immediate download)""")
|
||||
private HandlingType download;
|
||||
@Schema(description = """
|
||||
Handling for the update part of the provisioning process ('skip': do not update yet,
|
||||
'attempt': server asks to update, 'forced': server requests immediate update)""")
|
||||
private HandlingType update;
|
||||
@JsonProperty("chunks")
|
||||
@NotNull
|
||||
@Schema(description = "Software chunks of an update. In server mapped by Software Module")
|
||||
private List<DdiChunk> chunks;
|
||||
@Schema(description = """
|
||||
Separation of download and installation by defining a maintenance window for the installation. Status shows if
|
||||
currently in a window""")
|
||||
private DdiMaintenanceWindowStatus maintenanceWindow;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param download handling type
|
||||
* @param update handling type
|
||||
* @param chunks to handle.
|
||||
* @param maintenanceWindow specifying whether there is a maintenance schedule associated.
|
||||
* If it is, the value is either 'available' (i.e. the
|
||||
* maintenance window is now available as per defined schedule
|
||||
* and the update can progress) or 'unavailable' (implying that
|
||||
* maintenance window is not available now and update should not
|
||||
* be attempted). If there is no maintenance schedule defined,
|
||||
* the parameter is null.
|
||||
*/
|
||||
public DdiDeployment(final HandlingType download, final HandlingType update, final List<DdiChunk> chunks,
|
||||
final DdiMaintenanceWindowStatus maintenanceWindow) {
|
||||
this.download = download;
|
||||
this.update = update;
|
||||
this.chunks = chunks;
|
||||
this.maintenanceWindow = maintenanceWindow;
|
||||
}
|
||||
|
||||
public List<DdiChunk> getChunks() {
|
||||
if (chunks == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(chunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* The handling type for the update action.
|
||||
*/
|
||||
public enum HandlingType {
|
||||
|
||||
/**
|
||||
* Not necessary for the command.
|
||||
*/
|
||||
SKIP("skip"),
|
||||
|
||||
/**
|
||||
* Try to execute (local applications may intervene by SP control API).
|
||||
*/
|
||||
ATTEMPT("attempt"),
|
||||
|
||||
/**
|
||||
* Execution independent of local intervention attempts.
|
||||
*/
|
||||
FORCED("forced");
|
||||
|
||||
private String name;
|
||||
|
||||
HandlingType(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of the maintenance window for action.
|
||||
*/
|
||||
public enum DdiMaintenanceWindowStatus {
|
||||
/**
|
||||
* A window is currently available, target can go ahead with
|
||||
* installation.
|
||||
*/
|
||||
AVAILABLE("available"),
|
||||
|
||||
/**
|
||||
* A window is not available, target should wait and skip the
|
||||
* installation.
|
||||
*/
|
||||
UNAVAILABLE("unavailable");
|
||||
|
||||
private String status;
|
||||
|
||||
DdiMaintenanceWindowStatus(final String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return status of maintenance window.
|
||||
*/
|
||||
@JsonValue
|
||||
public String getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* 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 jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
|
||||
/**
|
||||
* Update action resource.
|
||||
*/
|
||||
@NoArgsConstructor // needed for json create
|
||||
@Getter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonPropertyOrder({ "id", "deployment", "actionHistory" })
|
||||
@Schema(example = """
|
||||
{
|
||||
"id" : "8",
|
||||
"deployment" : {
|
||||
"download" : "forced",
|
||||
"update" : "forced",
|
||||
"maintenanceWindow" : "available",
|
||||
"chunks" : [ {
|
||||
"part" : "jvm",
|
||||
"version" : "1.0.75",
|
||||
"name" : "oneapp runtime",
|
||||
"artifacts" : [ {
|
||||
"filename" : "binary.tgz",
|
||||
"hashes" : {
|
||||
"sha1" : "986a1ade8b8a2f758ce951340cc5e21335cc2a00",
|
||||
"md5" : "d04440e6533863247655ac5fd4345bcc",
|
||||
"sha256" : "b3a04740a19e36057ccf258701922f3cd2f1a880536be53a3ca8d50f6b615975"
|
||||
},
|
||||
"size" : 13,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/binary.tgz.MD5SUM"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"filename" : "file.signature",
|
||||
"hashes" : {
|
||||
"sha1" : "986a1ade8b8a2f758ce951340cc5e21335cc2a00",
|
||||
"md5" : "d04440e6533863247655ac5fd4345bcc",
|
||||
"sha256" : "b3a04740a19e36057ccf258701922f3cd2f1a880536be53a3ca8d50f6b615975"
|
||||
},
|
||||
"size" : 13,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/23/filename/file.signature.MD5SUM"
|
||||
}
|
||||
}
|
||||
} ]
|
||||
}, {
|
||||
"part" : "os",
|
||||
"version" : "1.0.79",
|
||||
"name" : "one Firmware",
|
||||
"artifacts" : [ {
|
||||
"filename" : "binary.tgz",
|
||||
"hashes" : {
|
||||
"sha1" : "574cd34be20f75d101ed23518339cc38c5157bdb",
|
||||
"md5" : "a0637c1ccb9fd53e2ba6f45712516989",
|
||||
"sha256" : "498014801aab66be1d7fbea56b1aa5959651b6fd710308e196a8c414029e7291"
|
||||
},
|
||||
"size" : 13,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/binary.tgz.MD5SUM"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"filename" : "file.signature",
|
||||
"hashes" : {
|
||||
"sha1" : "574cd34be20f75d101ed23518339cc38c5157bdb",
|
||||
"md5" : "a0637c1ccb9fd53e2ba6f45712516989",
|
||||
"sha256" : "498014801aab66be1d7fbea56b1aa5959651b6fd710308e196a8c414029e7291"
|
||||
},
|
||||
"size" : 13,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/24/filename/file.signature.MD5SUM"
|
||||
}
|
||||
}
|
||||
} ]
|
||||
}, {
|
||||
"part" : "bApp",
|
||||
"version" : "1.0.91",
|
||||
"name" : "oneapplication",
|
||||
"artifacts" : [ {
|
||||
"filename" : "binary.tgz",
|
||||
"hashes" : {
|
||||
"sha1" : "e3ba7ff5839c210c98e254dde655147ffc49f5c9",
|
||||
"md5" : "020017c498e6b0b8f76168fd55fa6fd1",
|
||||
"sha256" : "80406288820379a82bbcbfbf7e8690146e46256f505de1c6d430c0168a74f6dd"
|
||||
},
|
||||
"size" : 11,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/binary.tgz.MD5SUM"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"filename" : "file.signature",
|
||||
"hashes" : {
|
||||
"sha1" : "e3ba7ff5839c210c98e254dde655147ffc49f5c9",
|
||||
"md5" : "020017c498e6b0b8f76168fd55fa6fd1",
|
||||
"sha256" : "80406288820379a82bbcbfbf7e8690146e46256f505de1c6d430c0168a74f6dd"
|
||||
},
|
||||
"size" : 11,
|
||||
"_links" : {
|
||||
"download" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature"
|
||||
},
|
||||
"download-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature"
|
||||
},
|
||||
"md5sum-http" : {
|
||||
"href" : "http://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature.MD5SUM"
|
||||
},
|
||||
"md5sum" : {
|
||||
"href" : "https://link-to-cdn.com/api/v1/TENANT_ID/download/controller/CONTROLLER_ID/softwaremodules/22/filename/file.signature.MD5SUM"
|
||||
}
|
||||
}
|
||||
} ],
|
||||
"metadata" : [ {
|
||||
"key" : "aMetadataKey",
|
||||
"value" : "Metadata value as defined in software module"
|
||||
} ]
|
||||
} ]
|
||||
},
|
||||
"actionHistory" : {
|
||||
"status" : "RUNNING",
|
||||
"messages" : [ "Reboot", "Write firmware", "Download done", "Download failed. ErrorCode #5876745. Retry", "Started download", "Assignment initiated by user 'TestPrincipal'" ]
|
||||
}
|
||||
}""")
|
||||
public class DdiDeploymentBase extends RepresentationModel<DdiDeploymentBase> {
|
||||
|
||||
@JsonProperty("id")
|
||||
@NotNull
|
||||
@Schema(description = "Id of the action", example = "8")
|
||||
private String id;
|
||||
|
||||
@JsonProperty("deployment")
|
||||
@NotNull
|
||||
@Schema(description = "Detailed deployment operation")
|
||||
private DdiDeployment deployment;
|
||||
|
||||
/**
|
||||
* Action history containing current action status and a list of feedback
|
||||
* messages received earlier from the controller.
|
||||
*/
|
||||
@JsonProperty("actionHistory")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(description = "Current deployment state")
|
||||
private DdiActionHistory actionHistory;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param id of the update action
|
||||
* @param deployment details
|
||||
* @param actionHistory containing current action status and a list of feedback
|
||||
* messages received earlier from the controller.
|
||||
*/
|
||||
public DdiDeploymentBase(final String id, final DdiDeployment deployment, final DdiActionHistory actionHistory) {
|
||||
this.id = id;
|
||||
this.deployment = deployment;
|
||||
this.actionHistory = actionHistory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 jakarta.validation.constraints.NotNull;
|
||||
|
||||
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;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Additional metadata to be provided for the target/device.
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiMetadata {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Schema(description = "Key of meta data entry")
|
||||
private final String key;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Schema(description = "Value of meta data entry")
|
||||
private final String value;
|
||||
|
||||
@JsonCreator
|
||||
public DdiMetadata(@JsonProperty("key") final String key, @JsonProperty("value") final String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/**
|
||||
* Polling interval for the SP target.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(description = "Suggested sleep time between polls")
|
||||
public class DdiPolling {
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description = "Sleep time in HH:MM:SS notation", pattern = "HH:MM:SS", example = "12:00:00")
|
||||
private String sleep;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param sleep between polls
|
||||
*/
|
||||
public DdiPolling(final String sleep) {
|
||||
this.sleep = sleep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public DdiPolling() {
|
||||
// needed for json create
|
||||
}
|
||||
|
||||
public String getSleep() {
|
||||
return sleep;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 jakarta.validation.constraints.NotNull;
|
||||
|
||||
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;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Action fulfillment progress by means of gives the achieved amount of maximal
|
||||
* of possible levels.
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiProgress {
|
||||
|
||||
@NotNull
|
||||
@Schema(description = "Achieved amount", example = "2")
|
||||
private final Integer cnt;
|
||||
|
||||
@Schema(description = "Maximum levels", example = "5")
|
||||
private final Integer of;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param cnt achieved amount
|
||||
* @param of maximum levels
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiProgress(@JsonProperty("cnt") final Integer cnt, @JsonProperty("of") final Integer of) {
|
||||
this.cnt = cnt;
|
||||
this.of = of;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Result information of the action progress which can by an intermediate or
|
||||
* final update.
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DdiResult {
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(description = "Result of the action execution")
|
||||
private final FinalResult finished;
|
||||
@Schema(description = "Progress assumption of the device (currently not supported)")
|
||||
private final DdiProgress progress;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param finished as final result
|
||||
* @param progress if not yet finished
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiResult(@JsonProperty("finished") final FinalResult finished,
|
||||
@JsonProperty("progress") final DdiProgress progress) {
|
||||
this.finished = finished;
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defined status of the final result.
|
||||
*/
|
||||
public enum FinalResult {
|
||||
/**
|
||||
* Execution was successful.
|
||||
*/
|
||||
SUCCESS("success"),
|
||||
|
||||
/**
|
||||
* Execution terminated with errors or without the expected result.
|
||||
*/
|
||||
FAILURE("failure"),
|
||||
|
||||
/**
|
||||
* No final result could be determined (yet).
|
||||
*/
|
||||
NONE("none");
|
||||
|
||||
private String name;
|
||||
|
||||
FinalResult(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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 java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Details status information concerning the action processing.
|
||||
*/
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(description = "Target action status")
|
||||
public class DdiStatus {
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(description = "Status of the action execution")
|
||||
private final ExecutionStatus execution;
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(description = "Result of the action execution")
|
||||
private final DdiResult result;
|
||||
@Schema(description = "(Optional) Individual status code", example = "200")
|
||||
private final Integer code;
|
||||
@Schema(description = "List of details message information", example = "[ \"Some feedback\" ]")
|
||||
private final List<String> details;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param execution status
|
||||
* @param result information
|
||||
* @param code as optional code (can be null)
|
||||
* @param details as optional addition
|
||||
*/
|
||||
@JsonCreator
|
||||
public DdiStatus(@JsonProperty("execution") final ExecutionStatus execution,
|
||||
@JsonProperty("result") final DdiResult result, @JsonProperty("code") final Integer code,
|
||||
@JsonProperty("details") final List<String> details) {
|
||||
this.execution = execution;
|
||||
this.result = result;
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public List<String> getDetails() {
|
||||
if (details == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(details);
|
||||
}
|
||||
|
||||
/**
|
||||
* The element status contains information about the execution of the
|
||||
* operation.
|
||||
*/
|
||||
public enum ExecutionStatus {
|
||||
/**
|
||||
* Execution of the action has finished.
|
||||
*/
|
||||
CLOSED("closed"),
|
||||
|
||||
/**
|
||||
* Execution has started but has not yet finished.
|
||||
*/
|
||||
PROCEEDING("proceeding"),
|
||||
|
||||
/**
|
||||
* Execution was suspended from outside.
|
||||
*/
|
||||
CANCELED("canceled"),
|
||||
|
||||
/**
|
||||
* Action has been noticed and is intended to run.
|
||||
*/
|
||||
SCHEDULED("scheduled"),
|
||||
|
||||
/**
|
||||
* Action was not accepted.
|
||||
*/
|
||||
REJECTED("rejected"),
|
||||
|
||||
/**
|
||||
* Action is started after a reset, power loss, etc.
|
||||
*/
|
||||
RESUMED("resumed"),
|
||||
|
||||
/**
|
||||
* The action has been downloaded by the target.
|
||||
*/
|
||||
DOWNLOADED("downloaded"),
|
||||
|
||||
/**
|
||||
* Target starts to download.
|
||||
*/
|
||||
DOWNLOAD("download");
|
||||
|
||||
private String name;
|
||||
|
||||
ExecutionStatus(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2018 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.JsonValue;
|
||||
|
||||
/**
|
||||
* Enumerates the supported update modes. Each mode represents an attribute
|
||||
* update strategy.
|
||||
*
|
||||
* @see DdiConfigData
|
||||
*/
|
||||
public enum DdiUpdateMode {
|
||||
|
||||
/**
|
||||
* Merge update strategy
|
||||
*/
|
||||
MERGE("merge"),
|
||||
|
||||
/**
|
||||
* Replacement update strategy
|
||||
*/
|
||||
REPLACE("replace"),
|
||||
|
||||
/**
|
||||
* Removal update strategy
|
||||
*/
|
||||
REMOVE("remove");
|
||||
|
||||
private String name;
|
||||
|
||||
DdiUpdateMode(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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.rest.api;
|
||||
|
||||
/**
|
||||
* Constants for the direct device integration rest resources.
|
||||
*/
|
||||
public final class DdiRestConstants {
|
||||
|
||||
/**
|
||||
* The base URL mapping of the direct device integration rest resources.
|
||||
*/
|
||||
public static final String BASE_V1_REQUEST_MAPPING = "/{tenant}/controller/v1";
|
||||
|
||||
/**
|
||||
* Deployment action resources.
|
||||
*/
|
||||
public static final String DEPLOYMENT_BASE_ACTION = "deploymentBase";
|
||||
|
||||
/**
|
||||
* Confirmation base resource.
|
||||
*/
|
||||
public static final String CONFIRMATION_BASE = "confirmationBase";
|
||||
|
||||
/**
|
||||
* Activate auto-confirm
|
||||
*/
|
||||
public static final String AUTO_CONFIRM_ACTIVATE = "activateAutoConfirm";
|
||||
|
||||
/**
|
||||
* Deactivate auto-confirm
|
||||
*/
|
||||
public static final String AUTO_CONFIRM_DEACTIVATE = "deactivateAutoConfirm";
|
||||
|
||||
/**
|
||||
* Installed action resources.
|
||||
*/
|
||||
public static final String INSTALLED_BASE_ACTION = "installedBase";
|
||||
|
||||
/**
|
||||
* Cancel action resources.
|
||||
*/
|
||||
public static final String CANCEL_ACTION = "cancelAction";
|
||||
|
||||
/**
|
||||
* Feedback channel.
|
||||
*/
|
||||
public static final String FEEDBACK = "feedback";
|
||||
|
||||
/**
|
||||
* File suffix for MDH hash download (see Linux md5sum).
|
||||
*/
|
||||
public static final String ARTIFACT_MD5_DWNL_SUFFIX = ".MD5SUM";
|
||||
|
||||
/**
|
||||
* Config data action resources.
|
||||
*/
|
||||
public static final String CONFIG_DATA_ACTION = "configData";
|
||||
|
||||
/**
|
||||
* Default value specifying that no action history to be sent as part of
|
||||
* response to deploymentBase
|
||||
* {@link DdiRootControllerRestApi#getControllerDeploymentBaseAction}.
|
||||
* {@link DdiRootControllerRestApi#getConfirmationBaseAction}.
|
||||
*/
|
||||
public static final String NO_ACTION_HISTORY = "0";
|
||||
|
||||
/**
|
||||
* Media type for CBOR content. Unfortunately, there is no other constant we
|
||||
* can reuse - even the Jackson data converter simply hardcodes this.
|
||||
*/
|
||||
public static final String MEDIA_TYPE_CBOR = "application/cbor";
|
||||
|
||||
private DdiRestConstants() {
|
||||
// constant class, private constructor.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,806 @@
|
||||
/**
|
||||
* 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.rest.api;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActivateAutoConfirmation;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAssignedVersion;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfigData;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBase;
|
||||
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.rest.json.model.ExceptionInfo;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/**
|
||||
* REST resource handling for root controller CRUD operations.
|
||||
*/
|
||||
// no request mapping specified here to avoid CVE-2021-22044 in Feign client
|
||||
@Tag(name = "DDI Root Controller", description = "REST resource handling for root controller CRUD operations")
|
||||
public interface DdiRootControllerRestApi {
|
||||
|
||||
/**
|
||||
* Returns all artifacts of a given software module and target.
|
||||
*
|
||||
* @param tenant of the client
|
||||
* @param controllerId of the target that matches to controller id
|
||||
* @param softwareModuleId of the software module
|
||||
* @return the response
|
||||
*/
|
||||
@Operation(summary = "Return all artifacts of a given software module and target",
|
||||
description = "Returns all artifacts that are assigned to the software module")
|
||||
@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 = "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 = "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)))
|
||||
})
|
||||
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
|
||||
+ "/{controllerId}/softwaremodules/{softwareModuleId}/artifacts", produces = { MediaTypes.HAL_JSON_VALUE,
|
||||
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<List<DdiArtifact>> getSoftwareModulesArtifacts(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("softwareModuleId") final Long softwareModuleId);
|
||||
|
||||
/**
|
||||
* Root resource for an individual {@link Target}.
|
||||
*
|
||||
* @param tenant of the request
|
||||
* @param controllerId of the target that matches to controller id
|
||||
* @return the response
|
||||
*/
|
||||
@Operation(summary = "Root resource for an individual Target", description = """
|
||||
This base resource can be regularly polled by the controller on the provisioning target or device in order to
|
||||
retrieve actions that need to be executed. Those are provided as a list of links to give more detailed
|
||||
information about the action. Links are only available for initial configuration, open actions, or the latest
|
||||
installed action, respectively. The resource supports Etag based modification checks in order to save traffic.
|
||||
|
||||
Note: deployments have to be confirmed in order to move on to the next action. Cancellations have to be
|
||||
confirmed or rejected.""")
|
||||
@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 = "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 = "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)))
|
||||
})
|
||||
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}", produces = {
|
||||
MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<DdiControllerBase> getControllerBase(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId);
|
||||
|
||||
/**
|
||||
* Handles GET {@link DdiArtifact} download request. This could be full or
|
||||
* partial (as specified by RFC7233 (Range Requests)) download request.
|
||||
*
|
||||
* @param tenant of the request
|
||||
* @param controllerId of the target
|
||||
* @param softwareModuleId of the parent software module
|
||||
* @param fileName of the related local artifact
|
||||
* @return response of the servlet which in case of success is status code
|
||||
* {@link HttpStatus#OK} or in case of partial download
|
||||
* {@link HttpStatus#PARTIAL_CONTENT}.
|
||||
*/
|
||||
@Operation(summary = "Artifact download", description = "Handles GET DdiArtifact download request. This could be " +
|
||||
"full or partial (as specified by RFC7233 (Range Requests)) download request.")
|
||||
@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 Module 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 = "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)))
|
||||
})
|
||||
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
|
||||
+ "/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{fileName}")
|
||||
ResponseEntity<InputStream> downloadArtifact(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("softwareModuleId") final Long softwareModuleId,
|
||||
@PathVariable("fileName") final String fileName);
|
||||
|
||||
/**
|
||||
* Handles GET {@link DdiArtifact} MD5 checksum file download request.
|
||||
*
|
||||
* @param tenant of the request
|
||||
* @param controllerId of the target
|
||||
* @param softwareModuleId of the parent software module
|
||||
* @param fileName of the related local artifact
|
||||
* @return {@link ResponseEntity} with status {@link HttpStatus#OK} if
|
||||
* successful
|
||||
*/
|
||||
@Operation(summary = "MD5 checksum download",
|
||||
description = "Handles GET {@link DdiArtifact} MD5 checksum file download request.")
|
||||
@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 Module 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 = "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)))
|
||||
})
|
||||
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
|
||||
+ "/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{fileName}"
|
||||
+ DdiRestConstants.ARTIFACT_MD5_DWNL_SUFFIX, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
ResponseEntity<Void> downloadArtifactMd5(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("softwareModuleId") final Long softwareModuleId,
|
||||
@PathVariable("fileName") final String fileName);
|
||||
|
||||
/**
|
||||
* Resource for software module.
|
||||
*
|
||||
* @param tenant of the request
|
||||
* @param controllerId of the target
|
||||
* @param actionId of the {@link DdiDeploymentBase} that matches to active
|
||||
* actions.
|
||||
* @param resource an hashcode of the resource which indicates if the action has
|
||||
* been changed, e.g. from 'soft' to 'force' and the eTag needs
|
||||
* to be re-generated
|
||||
* @param actionHistoryMessageCount specifies the number of messages to be returned from action
|
||||
* history. Regardless of the passed value, in order to restrict
|
||||
* resource utilization by controllers, maximum number of
|
||||
* messages that are retrieved from database is limited by
|
||||
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
|
||||
*
|
||||
* actionHistoryMessageCount less than zero: retrieves the
|
||||
* maximum allowed number of action status messages from history;
|
||||
*
|
||||
* actionHistoryMessageCount equal to zero: does not retrieve any
|
||||
* message;
|
||||
*
|
||||
* actionHistoryMessageCount greater than zero: retrieves the
|
||||
* specified number of messages, limited by maximum allowed
|
||||
* number.
|
||||
* @return the response
|
||||
*/
|
||||
@Operation(summary = "Resource for software module (Deployment Base)", description = """
|
||||
Core resource for deployment operations. Contains all information necessary in order to execute the operation.
|
||||
|
||||
Keep in mind that the provided download links for the artifacts are generated dynamically by the update server.
|
||||
Host, port and path and not guaranteed to be similar to the provided examples below but will be defined at
|
||||
runtime.
|
||||
""")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = """
|
||||
Successfully retrieved
|
||||
|
||||
In case a device provides state information on the feedback channel and won’t store it locally,
|
||||
a query for, e.q, the last 10 messages, could be used which will include the previously provided by the
|
||||
device,
|
||||
feedback.
|
||||
|
||||
In addition to the straight forward approach to inform the device to download and install the software
|
||||
in one transaction hawkBit supports the separation of download and installation into separate steps.
|
||||
|
||||
This feature is called Maintenance Window where the device is informed to download the software first
|
||||
and then when it enters a defined (maintenance) window the installation triggers follows as usual.
|
||||
"""),
|
||||
@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 = "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 = "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)))
|
||||
})
|
||||
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
|
||||
+ DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE,
|
||||
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<DdiDeploymentBase> getControllerDeploymentBaseAction(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId,
|
||||
@RequestParam(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "c", required = false, defaultValue = "-1") final int resource,
|
||||
@RequestParam(
|
||||
value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "actionHistory",
|
||||
defaultValue = DdiRestConstants.NO_ACTION_HISTORY)
|
||||
@Schema(description = """
|
||||
(Optional) GET parameter to retrieve a given number of messages which are previously provided by the
|
||||
device. Useful if the devices sent state information to the feedback channel and never stored them
|
||||
locally.""") final Integer actionHistoryMessageCount);
|
||||
|
||||
/**
|
||||
* This is the feedback channel for the {@link DdiDeploymentBase} action.
|
||||
*
|
||||
* @param tenant of the client
|
||||
* @param feedback to provide
|
||||
* @param controllerId of the target that matches to controller id
|
||||
* @param actionId of the action we have feedback for
|
||||
* @return the response
|
||||
*/
|
||||
@Operation(summary = "Feedback channel for the DeploymentBase action", description = """
|
||||
Feedback channel. It is up to the device how much intermediate feedback is provided.
|
||||
However, the action will be kept open until the controller on the device reports a finished (either successful
|
||||
or error).
|
||||
""")
|
||||
@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 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.DEPLOYMENT_BASE_ACTION + "/{actionId}/" + DdiRestConstants.FEEDBACK, consumes = {
|
||||
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<Void> postDeploymentBaseActionFeedback(@Valid final DdiActionFeedback feedback,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId);
|
||||
|
||||
/**
|
||||
* This is the feedback channel for the config data action.
|
||||
*
|
||||
* @param tenant of the client
|
||||
* @param configData as body
|
||||
* @param controllerId to provide data for
|
||||
* @return status of the request
|
||||
*/
|
||||
@Operation(summary = "Feedback channel for the config data action", description = """
|
||||
The usual behaviour is that when a new device registers at the server it is requested to provide the meta
|
||||
information that will allow the server to identify the device on a hardware level (e.g. hardware revision,
|
||||
mac address, serial number etc.).""")
|
||||
@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 = "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 = "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)))
|
||||
})
|
||||
@PutMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
|
||||
+ DdiRestConstants.CONFIG_DATA_ACTION, consumes = { MediaType.APPLICATION_JSON_VALUE,
|
||||
DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<Void> putConfigData(@Valid final DdiConfigData configData,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId);
|
||||
|
||||
/**
|
||||
* RequestMethod.GET method for the {@link DdiCancel} action.
|
||||
*
|
||||
* @param tenant of the request
|
||||
* @param controllerId ID of the calling target
|
||||
* @param actionId of the action
|
||||
* @return the {@link DdiCancel} response
|
||||
*/
|
||||
@Operation(summary = "Cancel an action", description = """
|
||||
The Hawkbit server might cancel an operation, e.g. an unfinished update has a successor. It is up to the
|
||||
provisioning target to decide to accept the cancelation or reject it.""")
|
||||
@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 = "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 = "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)))
|
||||
})
|
||||
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION
|
||||
+ "/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE,
|
||||
DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<DdiCancel> getControllerCancelAction(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId);
|
||||
|
||||
/**
|
||||
* RequestMethod.POST method receiving the {@link DdiActionFeedback} from
|
||||
* the target.
|
||||
*
|
||||
* @param feedback the {@link DdiActionFeedback} from the target.
|
||||
* @param tenant of the client
|
||||
* @param controllerId the ID of the calling target
|
||||
* @param actionId of the action we have feedback for
|
||||
* @return the {@link DdiActionFeedback} response
|
||||
*/
|
||||
@Operation(summary = "Feedback channel for cancel actions", description = """
|
||||
It is up to the device how much intermediate feedback is provided. However, the action will be kept open
|
||||
until the controller on the device reports a finished (either successful or error) or rejects the action,
|
||||
e.g. the canceled actions have been started already.""")
|
||||
@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 = "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 = "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.CANCEL_ACTION
|
||||
+ "/{actionId}/" + DdiRestConstants.FEEDBACK, consumes = { MediaType.APPLICATION_JSON_VALUE,
|
||||
DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<Void> postCancelActionFeedback(@Valid final DdiActionFeedback feedback,
|
||||
@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId);
|
||||
|
||||
/**
|
||||
* Resource for installed distribution set to retrieve the last successfully
|
||||
* finished action.
|
||||
*
|
||||
* @param tenant of the request
|
||||
* @param controllerId of the target
|
||||
* @param actionId of the {@link DdiDeploymentBase} that matches to installed
|
||||
* action.
|
||||
* @param actionHistoryMessageCount specifies the number of messages to be returned from action
|
||||
* history. Regardless of the passed value, in order to restrict
|
||||
* resource utilization by controllers, maximum number of
|
||||
* messages that are retrieved from database is limited by
|
||||
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
|
||||
*
|
||||
* actionHistoryMessageCount less than zero: retrieves the
|
||||
* maximum allowed number of action status messages from history;
|
||||
*
|
||||
* actionHistoryMessageCount equal to zero: does not retrieve any
|
||||
* message;
|
||||
*
|
||||
* actionHistoryMessageCount greater than zero: retrieves the
|
||||
* specified number of messages, limited by maximum allowed
|
||||
* number.
|
||||
* @return the {@link DdiDeploymentBase}. The response is of same format as
|
||||
* for the /deploymentBase resource.
|
||||
*/
|
||||
@Operation(summary = "Previously installed action", description = """
|
||||
Resource to receive information of the previous installation. Can be used to re-retrieve artifacts of
|
||||
the already finished action, for example in case a re-installation is necessary. The response will be of
|
||||
the same format as the deploymentBase operation, providing the previous action that has been finished
|
||||
successfully. As the action is already finished, no further feedback is expected.
|
||||
|
||||
Keep in mind that the provided download links for the artifacts are generated dynamically by the update server.
|
||||
Host, port and path are not guaranteed to be similar to the provided examples below but will be defined at
|
||||
runtime.
|
||||
""")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = """
|
||||
The response body includes the detailed operation for the already finished action in the same format as
|
||||
for the deploymentBase operation.
|
||||
|
||||
In this case the (optional) query for the last 10 messages, previously provided by the device, are included.
|
||||
"""),
|
||||
@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 = "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 = "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)))
|
||||
})
|
||||
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
|
||||
+ DdiRestConstants.INSTALLED_BASE_ACTION + "/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE,
|
||||
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<DdiDeploymentBase> getControllerInstalledAction(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId,
|
||||
@RequestParam(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
|
||||
+ "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount);
|
||||
|
||||
/**
|
||||
* Returns the confirmation base with the current auto-confirmation state
|
||||
* for a given controllerId and toggle links. In case there are actions
|
||||
* present where the confirmation is required, a reference to it will be
|
||||
* returned as well.
|
||||
*
|
||||
* @param tenant the controllerId is corresponding too
|
||||
* @param controllerId to check the state for
|
||||
* @return the state as {@link DdiAutoConfirmationState}
|
||||
*/
|
||||
@Operation(summary = "Resource to request confirmation specific information for the controller", description = """
|
||||
Core resource for confirmation related operations. While active actions awaiting confirmation will be
|
||||
referenced, the current auto-confirmation status will be shown. In case auto-confirmation is active, details
|
||||
like the initiator, remark and date of activation (as unix timestamp) will be provided.
|
||||
Reference links to switch the auto-confirmation state are exposed as well.
|
||||
""")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = """
|
||||
The response body in case auto-confirmation is active is richer - it contains additional information
|
||||
such as initiator, remark and when the auto-confirmation had been activated.
|
||||
"""),
|
||||
@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 = "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 = "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)))
|
||||
})
|
||||
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
|
||||
+ DdiRestConstants.CONFIRMATION_BASE, produces = { MediaTypes.HAL_JSON_VALUE,
|
||||
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<DdiConfirmationBase> getConfirmationBase(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId);
|
||||
|
||||
/**
|
||||
* Resource for confirmation of an action.
|
||||
*
|
||||
* @param tenant of the request
|
||||
* @param controllerId of the target
|
||||
* @param actionId of the {@link DdiConfirmationBaseAction} that matches to
|
||||
* active actions in WAITING_FOR_CONFIRMATION status.
|
||||
* @param resource an hashcode of the resource which indicates if the action has
|
||||
* been changed, e.g. from 'soft' to 'force' and the eTag needs
|
||||
* to be re-generated
|
||||
* @param actionHistoryMessageCount specifies the number of messages to be returned from action
|
||||
* history. Regardless of the passed value, in order to restrict
|
||||
* resource utilization by controllers, maximum number of
|
||||
* messages that are retrieved from database is limited by
|
||||
* {@link RepositoryConstants#MAX_ACTION_HISTORY_MSG_COUNT}.
|
||||
*
|
||||
* actionHistoryMessageCount less than zero: retrieves the
|
||||
* maximum allowed number of action status messages from history;
|
||||
*
|
||||
* actionHistoryMessageCount equal to zero: does not retrieve any
|
||||
* message;
|
||||
*
|
||||
* actionHistoryMessageCount greater than zero: retrieves the
|
||||
* specified number of messages, limited by maximum allowed
|
||||
* number.
|
||||
* @return the response
|
||||
*/
|
||||
@Operation(summary = "Confirmation status of an action", description = """
|
||||
Resource to receive information about a pending confirmation. The response will be of the same format as the
|
||||
deploymentBase operation. The controller should provide feedback about the confirmation first, before
|
||||
processing the deployment.
|
||||
|
||||
Keep in mind that the provided download links for the artifacts are generated dynamically by the update server.
|
||||
Host, port and path are not guaranteed to be similar to the provided examples below but will be defined at
|
||||
runtime.
|
||||
""")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "The response body includes the detailed information about " +
|
||||
"the action awaiting confirmation in the same format as for the deploymentBase operation."),
|
||||
@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 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 = "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)))
|
||||
})
|
||||
@GetMapping(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING + "/{controllerId}/"
|
||||
+ DdiRestConstants.CONFIRMATION_BASE + "/{actionId}", produces = { MediaTypes.HAL_JSON_VALUE,
|
||||
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<DdiConfirmationBaseAction> getConfirmationBaseAction(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId,
|
||||
@RequestParam(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
|
||||
+ "c", required = false, defaultValue = "-1") final int resource,
|
||||
@RequestParam(value = DdiRestConstants.BASE_V1_REQUEST_MAPPING
|
||||
+ "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount);
|
||||
|
||||
/**
|
||||
* This is the feedback channel for the {@link DdiConfirmationBaseAction}
|
||||
* action.
|
||||
*
|
||||
* @param tenant of the client
|
||||
* @param feedback to provide
|
||||
* @param controllerId of the target that matches to controller id
|
||||
* @param actionId of the action we have feedback for
|
||||
* @return the response
|
||||
*/
|
||||
@Operation(summary = "Feedback channel for actions waiting for confirmation", description = """
|
||||
The device will use this resource to either confirm or deny an action which is waiting for confirmation. The
|
||||
action will be transferred into the RUNNING state in case the device is confirming it. Afterwards it will be
|
||||
exposed by the deploymentBase.
|
||||
""")
|
||||
@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 Action 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.CONFIRMATION_BASE + "/{actionId}/" + DdiRestConstants.FEEDBACK, consumes = {
|
||||
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<Void> postConfirmationActionFeedback(@Valid final DdiConfirmationFeedback feedback,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId);
|
||||
|
||||
/**
|
||||
* Activate auto confirmation for a given controllerId. Will use the
|
||||
* provided initiator and remark field from the provided
|
||||
* {@link DdiActivateAutoConfirmation}. If not present, the values will be
|
||||
* prefilled with a default remark and the CONTROLLER as initiator.
|
||||
*
|
||||
* @param tenant the controllerId is corresponding too
|
||||
* @param controllerId to activate auto-confirmation for
|
||||
* @param body as {@link DdiActivateAutoConfirmation}
|
||||
* @return {@link org.springframework.http.HttpStatus#OK} if successful or
|
||||
* {@link org.springframework.http.HttpStatus#CONFLICT} in case
|
||||
* auto-confirmation was active already.
|
||||
*/
|
||||
@Operation(summary = "Interface to activate auto-confirmation for a specific device", description = """
|
||||
The device can use this resource to activate auto-confirmation. As a result all current active as well as
|
||||
future actions will automatically be confirmed by mentioning the initiator as triggered person. Actions will
|
||||
be automatically confirmed, as long as auto-confirmation is active.
|
||||
""")
|
||||
@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 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 = "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.CONFIRMATION_BASE + "/" + DdiRestConstants.AUTO_CONFIRM_ACTIVATE, consumes = {
|
||||
MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
|
||||
ResponseEntity<Void> activateAutoConfirmation(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@Valid @RequestBody(required = false) final DdiActivateAutoConfirmation body);
|
||||
|
||||
/**
|
||||
* Deactivate auto confirmation for a given controller id.
|
||||
*
|
||||
* @param tenant the controllerId is corresponding too
|
||||
* @param controllerId to disable auto-confirmation for
|
||||
* @return {@link org.springframework.http.HttpStatus#OK} if successfully executed
|
||||
*/
|
||||
@Operation(summary = "Interface to deactivate auto-confirmation for a specific controller", description = """
|
||||
The device can use this resource to deactivate auto-confirmation. All active actions will remain unchanged
|
||||
while all future actions need to be confirmed, before processing with the deployment.
|
||||
""")
|
||||
@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 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 = "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.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.
|
||||
This method is EXPERIMENTAL and may change in future releases.
|
||||
""")
|
||||
@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)))
|
||||
})
|
||||
@PutMapping(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);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serialization of DDI api model 'DdiActionFeedback'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serialization of DDI api Models")
|
||||
class DdiActionFeedbackTest {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model with minimal payload")
|
||||
void shouldSerializeAndDeserializeObjectWithoutOptionalValues() throws IOException {
|
||||
// Setup
|
||||
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, null, null, Lists.emptyList());
|
||||
final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(null, ddiStatus);
|
||||
|
||||
// Test
|
||||
final String serializedDdiActionFeedback = mapper.writeValueAsString(ddiActionFeedback);
|
||||
final DdiActionFeedback deserializedDdiActionFeedback = mapper.readValue(serializedDdiActionFeedback,
|
||||
DdiActionFeedback.class);
|
||||
|
||||
assertThat(deserializedDdiActionFeedback.getStatus()).hasToString(ddiStatus.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model with all values provided")
|
||||
void shouldSerializeAndDeserializeObjectWithOptionalValues() throws IOException {
|
||||
// Setup
|
||||
final String time = Instant.now().toString();
|
||||
final DdiResult ddiResult = new DdiResult(DdiResult.FinalResult.SUCCESS, new DdiProgress(10, 10));
|
||||
final DdiStatus ddiStatus = new DdiStatus(DdiStatus.ExecutionStatus.CLOSED, ddiResult, 200, Collections.singletonList("myMessage"));
|
||||
final DdiActionFeedback ddiActionFeedback = new DdiActionFeedback(time, ddiStatus);
|
||||
|
||||
// Test
|
||||
final String serializedDdiActionFeedback = mapper.writeValueAsString(ddiActionFeedback);
|
||||
final DdiActionFeedback deserializedDdiActionFeedback = mapper.readValue(serializedDdiActionFeedback,
|
||||
DdiActionFeedback.class);
|
||||
|
||||
assertThat(serializedDdiActionFeedback).contains(time);
|
||||
assertThat(deserializedDdiActionFeedback.getTime()).isEqualTo(time);
|
||||
assertThat(deserializedDdiActionFeedback.getStatus()).hasToString(ddiStatus.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
final String serializedDdiActionFeedback = "{\"time\":\"20190809T121314\",\"status\":{\"execution\": [closed],\"result\":null,\"details\":[]}}";
|
||||
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiActionFeedback, DdiActionFeedback.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization works if optional fields are not parsed")
|
||||
void shouldConvertItWithoutOptionalFieldTime() throws JsonProcessingException {
|
||||
// Setup
|
||||
final String serializedDdiActionFeedback = "{\n" + //
|
||||
" \"status\" : {\n" + //
|
||||
" \"result\" : {\n" + //
|
||||
" \"finished\" : \"none\"\n" + //
|
||||
" },\n" + //
|
||||
" \"execution\" : \"download\",\n" + //
|
||||
" \"details\" : [ \"Some message\" ]\n" + //
|
||||
" }\n" + //
|
||||
"}";//
|
||||
|
||||
assertThat(mapper.readValue(serializedDdiActionFeedback, DdiActionFeedback.class)).satisfies(deserializedDdiActionFeedback -> {
|
||||
assertThat(deserializedDdiActionFeedback.getTime()).isNull();
|
||||
assertThat(deserializedDdiActionFeedback.getStatus()).isNotNull();
|
||||
assertThat(deserializedDdiActionFeedback.getStatus().getResult()).isNotNull();
|
||||
assertThat(deserializedDdiActionFeedback.getStatus().getResult().getFinished()).isEqualTo(DdiResult.FinalResult.NONE);
|
||||
assertThat(deserializedDdiActionFeedback.getStatus().getResult().getProgress()).isNull();
|
||||
assertThat(deserializedDdiActionFeedback.getStatus().getCode()).isNull();
|
||||
assertThat(deserializedDdiActionFeedback.getStatus().getExecution()).isEqualTo(DdiStatus.ExecutionStatus.DOWNLOAD);
|
||||
assertThat(deserializedDdiActionFeedback.getStatus().getDetails()).hasSize(1);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiActionHistory'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiActionHistoryTest {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
final String actionStatus = "TestAction";
|
||||
final List<String> messages = Arrays.asList("Action status message 1", "Action status message 2");
|
||||
final DdiActionHistory ddiActionHistory = new DdiActionHistory(actionStatus, messages);
|
||||
|
||||
// Test
|
||||
final String serializedDdiActionHistory = OBJECT_MAPPER.writeValueAsString(ddiActionHistory);
|
||||
final DdiActionHistory deserializedDdiActionHistory = OBJECT_MAPPER.readValue(serializedDdiActionHistory,
|
||||
DdiActionHistory.class);
|
||||
|
||||
assertThat(serializedDdiActionHistory).contains(actionStatus, messages.get(0), messages.get(1));
|
||||
assertThat(deserializedDdiActionHistory.toString()).contains(actionStatus, messages.get(0), messages.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
final String serializedDdiActionHistory = """
|
||||
{
|
||||
"status": "SomeAction",
|
||||
"messages": [ "Some message"],
|
||||
"unknownProperty": "test"
|
||||
}""";
|
||||
|
||||
// Test
|
||||
final DdiActionHistory ddiActionHistory = OBJECT_MAPPER.readValue(serializedDdiActionHistory, DdiActionHistory.class);
|
||||
|
||||
assertThat(ddiActionHistory.toString()).contains("SomeAction", "Some message");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
final String serializedDdiActionFeedback = """
|
||||
{
|
||||
"status": [SomeAction],
|
||||
"messages": ["Some message"]
|
||||
}""";
|
||||
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> OBJECT_MAPPER.readValue(serializedDdiActionFeedback, DdiActionHistory.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiArtifactHash'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiArtifactHashTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
String sha1Hash = "11111";
|
||||
String md5Hash = "22222";
|
||||
String sha256Hash = "33333";
|
||||
DdiArtifactHash DdiArtifact = new DdiArtifactHash(sha1Hash, md5Hash, sha256Hash);
|
||||
|
||||
// Test
|
||||
String serializedDdiArtifact = mapper.writeValueAsString(DdiArtifact);
|
||||
DdiArtifactHash deserializedDdiArtifact = mapper.readValue(serializedDdiArtifact,
|
||||
DdiArtifactHash.class);
|
||||
|
||||
assertThat(serializedDdiArtifact).contains(sha1Hash, md5Hash, sha256Hash);
|
||||
assertThat(deserializedDdiArtifact.getSha1()).isEqualTo(sha1Hash);
|
||||
assertThat(deserializedDdiArtifact.getMd5()).isEqualTo(md5Hash);
|
||||
assertThat(deserializedDdiArtifact.getSha256()).isEqualTo(sha256Hash);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiArtifact = "{\"sha1\": \"123\", \"md5\": \"456\", \"sha256\": \"789\", \"unknownProperty\": \"test\"}";
|
||||
|
||||
// Test
|
||||
DdiArtifactHash ddiArtifact = mapper.readValue(serializedDdiArtifact, DdiArtifactHash.class);
|
||||
|
||||
assertThat(ddiArtifact.getSha1()).isEqualTo("123");
|
||||
assertThat(ddiArtifact.getMd5()).isEqualTo("456");
|
||||
assertThat(ddiArtifact.getSha256()).isEqualTo("789");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiArtifact = "{\"sha1\": [123], \"md5\": 456, \"sha256\": \"789\"";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiArtifact, DdiArtifactHash.class));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiArtifact'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiArtifactTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
String filename = "testfile.txt";
|
||||
DdiArtifactHash hashes = new DdiArtifactHash("123", "456", "789");
|
||||
Long size = 12345L;
|
||||
|
||||
DdiArtifact ddiArtifact = new DdiArtifact();
|
||||
ddiArtifact.setFilename(filename);
|
||||
ddiArtifact.setHashes(hashes);
|
||||
ddiArtifact.setSize(size);
|
||||
|
||||
// Test
|
||||
String serializedDdiArtifact = mapper.writeValueAsString(ddiArtifact);
|
||||
DdiArtifact deserializedDdiArtifact = mapper.readValue(serializedDdiArtifact, DdiArtifact.class);
|
||||
|
||||
assertThat(serializedDdiArtifact).contains(filename, "12345");
|
||||
assertThat(deserializedDdiArtifact.getFilename()).isEqualTo(filename);
|
||||
assertThat(deserializedDdiArtifact.getSize()).isEqualTo(size);
|
||||
assertThat(deserializedDdiArtifact.getHashes().getSha1()).isEqualTo(hashes.getSha1());
|
||||
assertThat(deserializedDdiArtifact.getHashes().getMd5()).isEqualTo(hashes.getMd5());
|
||||
assertThat(deserializedDdiArtifact.getHashes().getSha256()).isEqualTo(hashes.getSha256());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiArtifact = "{\"filename\":\"test.file\",\"hashes\":{\"sha1\":\"123\",\"md5\":\"456\",\"sha256\":\"789\"},\"size\":111,\"links\":[],\"unknownProperty\": \"test\"}";
|
||||
|
||||
// Test
|
||||
DdiArtifact ddiArtifact = mapper.readValue(serializedDdiArtifact, DdiArtifact.class);
|
||||
|
||||
assertThat(ddiArtifact.getFilename()).isEqualTo("test.file");
|
||||
assertThat(ddiArtifact.getSize()).isEqualTo(111);
|
||||
assertThat(ddiArtifact.getHashes().getSha1()).isEqualTo("123");
|
||||
assertThat(ddiArtifact.getHashes().getMd5()).isEqualTo("456");
|
||||
assertThat(ddiArtifact.getHashes().getSha256()).isEqualTo("789");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiArtifact = "{\"filename\": [\"test.file\"],\"hashes\":{\"sha1\":\"123\",\"md5\":\"456\",\"sha256\":\"789\"},\"size\":111,\"links\":[]}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiArtifact, DdiArtifact.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiCancelActionToStop'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiCancelActionToStopTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
String stopId = "1234";
|
||||
DdiCancelActionToStop ddiCancelActionToStop = new DdiCancelActionToStop(stopId);
|
||||
// Test
|
||||
String serializedDdiCancelActionToStop = mapper.writeValueAsString(ddiCancelActionToStop);
|
||||
DdiCancelActionToStop deserializedDdiCancelActionToStop = mapper.readValue(serializedDdiCancelActionToStop,
|
||||
DdiCancelActionToStop.class);
|
||||
|
||||
assertThat(serializedDdiCancelActionToStop).contains(stopId);
|
||||
assertThat(deserializedDdiCancelActionToStop.getStopId()).isEqualTo(stopId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiCancelActionToStop = "{\"stopId\":\"12345\",\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiCancelActionToStop ddiCancelActionToStop = mapper.readValue(serializedDdiCancelActionToStop,
|
||||
DdiCancelActionToStop.class);
|
||||
|
||||
assertThat(ddiCancelActionToStop.getStopId()).contains("12345");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiCancelActionToStop = "{\"stopId\": [\"12345\"]}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiCancelActionToStop, DdiCancelActionToStop.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiArtifact'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiCancelTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
String ddiCancelId = "1234";
|
||||
DdiCancelActionToStop ddiCancelActionToStop = new DdiCancelActionToStop("1234");
|
||||
DdiCancel ddiCancel = new DdiCancel(ddiCancelId, ddiCancelActionToStop);
|
||||
|
||||
// Test
|
||||
String serializedDdiCancel = mapper.writeValueAsString(ddiCancel);
|
||||
DdiCancel deserializedDdiCancel = mapper.readValue(serializedDdiCancel, DdiCancel.class);
|
||||
|
||||
assertThat(serializedDdiCancel).contains(ddiCancelId, ddiCancelActionToStop.getStopId());
|
||||
assertThat(deserializedDdiCancel.getId()).isEqualTo(ddiCancelId);
|
||||
assertThat(deserializedDdiCancel.getCancelAction().getStopId()).isEqualTo(ddiCancelActionToStop.getStopId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiCancel = "{\"id\":\"1234\",\"cancelAction\":{\"stopId\":\"1234\"}, \"unknownProperty\": \"test\"}";
|
||||
|
||||
// Test
|
||||
DdiCancel ddiCancel = mapper.readValue(serializedDdiCancel, DdiCancel.class);
|
||||
|
||||
assertThat(ddiCancel.getId()).isEqualTo("1234");
|
||||
assertThat(ddiCancel.getCancelAction().getStopId()).matches("1234");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiCancel = "{\"id\":[\"1234\"],\"cancelAction\":{\"stopId\":\"1234\"}}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiCancel, DdiCancel.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiChunk'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiChunkTest {
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
final String part = "1234";
|
||||
final String version = "1.0";
|
||||
final String name = "Dummy-Artifact";
|
||||
final List<DdiArtifact> dummyArtifacts = Collections.emptyList();
|
||||
final DdiChunk ddiChunk = new DdiChunk(part, version, name, null, dummyArtifacts, null);
|
||||
|
||||
// Test
|
||||
final String serializedDdiChunk = mapper.writeValueAsString(ddiChunk);
|
||||
final DdiChunk deserializedDdiChunk = mapper.readValue(serializedDdiChunk, DdiChunk.class);
|
||||
|
||||
assertThat(serializedDdiChunk).contains(part, version, name);
|
||||
assertThat(deserializedDdiChunk.getPart()).isEqualTo(part);
|
||||
assertThat(deserializedDdiChunk.getVersion()).isEqualTo(version);
|
||||
assertThat(deserializedDdiChunk.getName()).isEqualTo(name);
|
||||
assertThat(deserializedDdiChunk.getArtifacts().size()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
final String serializedDdiChunk = "{\"part\":\"1234\",\"version\":\"1.0\",\"name\":\"Dummy-Artifact\",\"artifacts\":[],\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
final DdiChunk ddiChunk = mapper.readValue(serializedDdiChunk, DdiChunk.class);
|
||||
|
||||
assertThat(ddiChunk.getPart()).isEqualTo("1234");
|
||||
assertThat(ddiChunk.getVersion()).isEqualTo("1.0");
|
||||
assertThat(ddiChunk.getName()).isEqualTo("Dummy-Artifact");
|
||||
assertThat(ddiChunk.getArtifacts().size()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
final String serializedDdiChunk = "{\"part\":[\"1234\"],\"version\":\"1.0\",\"name\":\"Dummy-Artifact\",\"artifacts\":[]}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class)
|
||||
.isThrownBy(() -> mapper.readValue(serializedDdiChunk, DdiChunk.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiConfigData'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiConfigDataTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
Map<String, String> data = new HashMap<>();
|
||||
data.put("test", "data");
|
||||
DdiConfigData ddiConfigData = new DdiConfigData(data, DdiUpdateMode.REPLACE);
|
||||
|
||||
// Test
|
||||
String serializedDdiConfigData = mapper.writeValueAsString(ddiConfigData);
|
||||
DdiConfigData deserializedDdiConfigData = mapper.readValue(serializedDdiConfigData, DdiConfigData.class);
|
||||
|
||||
assertThat(serializedDdiConfigData).contains("test", "data");
|
||||
assertThat(deserializedDdiConfigData.getMode()).isEqualTo(DdiUpdateMode.REPLACE);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with an additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiConfigData = "{\"data\":{\"test\":\"data\"},\"mode\":\"replace\",\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiConfigData ddiConfigData = mapper.readValue(serializedDdiConfigData, DdiConfigData.class);
|
||||
|
||||
assertThat(ddiConfigData.getMode()).isEqualTo(DdiUpdateMode.REPLACE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiConfigData = "{\"data\":{\"test\":\"data\"},\"mode\":[\"replace\"],\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiConfigData, DdiConfigData.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with removed unused status property")
|
||||
public void shouldDeserializeObjectWithStatusProperty() throws IOException {
|
||||
// We formerly falsely required a 'status' property object when using the
|
||||
// configData endpoint. It was removed as a requirement from code and
|
||||
// documentation, as it was unused. This test ensures we still behave correctly
|
||||
// (and just ignore the 'status' property) if it is still provided by the
|
||||
// client.
|
||||
|
||||
// Setup
|
||||
String serializedDdiConfigData = "{\"id\":123,\"time\":\"20190809T121314\","
|
||||
+ "\"status\":{\"execution\":\"closed\",\"result\":{\"finished\":\"success\",\"progress\":null},"
|
||||
+ "\"details\":[]},\"data\":{\"test\":\"data\"},\"mode\":\"replace\"}";
|
||||
|
||||
// Test
|
||||
DdiConfigData ddiConfigData = mapper.readValue(serializedDdiConfigData, DdiConfigData.class);
|
||||
|
||||
assertThat(ddiConfigData.getMode()).isEqualTo(DdiUpdateMode.REPLACE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiConfig'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiConfigTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
DdiPolling ddiPolling = new DdiPolling("10");
|
||||
DdiConfig ddiConfig = new DdiConfig(ddiPolling);
|
||||
|
||||
// Test
|
||||
String serializedDdiConfig = mapper.writeValueAsString(ddiConfig);
|
||||
DdiConfig deserializedDdiConfig = mapper.readValue(serializedDdiConfig, DdiConfig.class);
|
||||
|
||||
assertThat(serializedDdiConfig).contains("10");
|
||||
assertThat(deserializedDdiConfig.getPolling().getSleep()).isEqualTo("10");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiConfig = "{\"polling\":{\"sleep\":\"123\"},\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiConfig ddiConfig = mapper.readValue(serializedDdiConfig, DdiConfig.class);
|
||||
|
||||
assertThat(ddiConfig.getPolling().getSleep()).isEqualTo("123");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiConfig = "{\"polling\":{\"sleep\":[\"10\"]}}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiConfig, DdiConfig.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus.AVAILABLE;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.ATTEMPT;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.FORCED;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiConfirmationBase'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("CHeck JSON serialization of DDI api confirmation models")
|
||||
class DdiConfirmationBaseTest {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
final String id = "1234";
|
||||
final DdiDeployment ddiDeployment = new DdiDeployment(FORCED, ATTEMPT, Collections.emptyList(), AVAILABLE);
|
||||
final String actionStatus = "TestAction";
|
||||
final DdiActionHistory ddiActionHistory = new DdiActionHistory(actionStatus,
|
||||
Arrays.asList("Action status message 1", "Action status message 2"));
|
||||
final DdiConfirmationBaseAction ddiConfirmationBaseAction = new DdiConfirmationBaseAction(id, ddiDeployment,
|
||||
ddiActionHistory);
|
||||
|
||||
// Test
|
||||
String serializedDdiConfirmationBase = OBJECT_MAPPER.writeValueAsString(ddiConfirmationBaseAction);
|
||||
final DdiConfirmationBaseAction deserializedDdiConfigurationBase = OBJECT_MAPPER
|
||||
.readValue(serializedDdiConfirmationBase, DdiConfirmationBaseAction.class);
|
||||
|
||||
assertThat(serializedDdiConfirmationBase).contains(id, FORCED.getName(), ATTEMPT.getName(),
|
||||
AVAILABLE.getStatus(), actionStatus);
|
||||
assertThat(deserializedDdiConfigurationBase.getConfirmation().getDownload())
|
||||
.isEqualTo(ddiDeployment.getDownload());
|
||||
assertThat(deserializedDdiConfigurationBase.getConfirmation().getUpdate()).isEqualTo(ddiDeployment.getUpdate());
|
||||
assertThat(deserializedDdiConfigurationBase.getConfirmation().getMaintenanceWindow())
|
||||
.isEqualTo(ddiDeployment.getMaintenanceWindow());
|
||||
assertThat(deserializedDdiConfigurationBase.getActionHistory().toString())
|
||||
.isEqualTo(ddiActionHistory.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
final String serializedDdiConfirmationBase = "{" +
|
||||
"\"id\":\"1234\",\"confirmation\":{\"download\":\"forced\"," +
|
||||
"\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]}," +
|
||||
"\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\"," +
|
||||
"\"Action status message 2\"]},\"links\":[],\"unknownProperty\":\"test\"" +
|
||||
"}";
|
||||
|
||||
// Test
|
||||
final DdiConfirmationBaseAction ddiConfirmationBaseAction = OBJECT_MAPPER.readValue(serializedDdiConfirmationBase,
|
||||
DdiConfirmationBaseAction.class);
|
||||
|
||||
assertThat(ddiConfirmationBaseAction.getConfirmation().getDownload().getName()).isEqualTo(FORCED.getName());
|
||||
assertThat(ddiConfirmationBaseAction.getConfirmation().getUpdate().getName()).isEqualTo(ATTEMPT.getName());
|
||||
assertThat(ddiConfirmationBaseAction.getConfirmation().getMaintenanceWindow().getStatus())
|
||||
.isEqualTo(AVAILABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
final String serializedDdiConfirmationBase = "{" +
|
||||
"\"id\":[\"1234\"],\"confirmation\":{\"download\":\"forced\"," +
|
||||
"\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]}," +
|
||||
"\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\"," +
|
||||
"\"Action status message 2\"]},\"links\":[]" +
|
||||
"}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class)
|
||||
.isThrownBy(() -> OBJECT_MAPPER.readValue(serializedDdiConfirmationBase, DdiConfirmationBaseAction.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiControllerBase'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiControllerBaseTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
// Setup
|
||||
DdiPolling ddiPolling = new DdiPolling("10");
|
||||
DdiConfig ddiConfig = new DdiConfig(ddiPolling);
|
||||
DdiControllerBase ddiControllerBase = new DdiControllerBase(ddiConfig);
|
||||
|
||||
// Test
|
||||
String serializedDdiControllerBase = mapper.writeValueAsString(ddiControllerBase);
|
||||
DdiControllerBase deserializedDdiControllerBase = mapper.readValue(serializedDdiControllerBase,
|
||||
DdiControllerBase.class);
|
||||
|
||||
assertThat(serializedDdiControllerBase).contains(ddiPolling.getSleep());
|
||||
assertThat(deserializedDdiControllerBase.getConfig().getPolling().getSleep()).isEqualTo(ddiPolling.getSleep());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiControllerBase = "{\"config\":{\"polling\":{\"sleep\":\"123\"}},\"links\":[],\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiControllerBase ddiControllerBase = mapper.readValue(serializedDdiControllerBase, DdiControllerBase.class);
|
||||
|
||||
assertThat(ddiControllerBase.getConfig().getPolling().getSleep()).isEqualTo("123");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiControllerBase = "{\"config\":{\"polling\":{\"sleep\":[\"123\"]}},\"links\":[],\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiControllerBase, DdiControllerBase.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus.AVAILABLE;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.ATTEMPT;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.FORCED;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiDeploymentBase'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiDeploymentBaseTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
String id = "1234";
|
||||
DdiDeployment ddiDeployment = new DdiDeployment(FORCED, ATTEMPT, Collections.emptyList(), AVAILABLE);
|
||||
String actionStatus = "TestAction";
|
||||
DdiActionHistory ddiActionHistory = new DdiActionHistory(actionStatus,
|
||||
Arrays.asList("Action status message 1", "Action status message 2"));
|
||||
DdiDeploymentBase ddiDeploymentBase = new DdiDeploymentBase(id, ddiDeployment, ddiActionHistory);
|
||||
|
||||
// Test
|
||||
String serializedDdiDeploymentBase = mapper.writeValueAsString(ddiDeploymentBase);
|
||||
DdiDeploymentBase deserializedDdiDeploymentBase = mapper.readValue(serializedDdiDeploymentBase,
|
||||
DdiDeploymentBase.class);
|
||||
|
||||
assertThat(serializedDdiDeploymentBase).contains(id, FORCED.getName(), ATTEMPT.getName(), AVAILABLE.getStatus(),
|
||||
actionStatus);
|
||||
assertThat(deserializedDdiDeploymentBase.getDeployment().getDownload()).isEqualTo(ddiDeployment.getDownload());
|
||||
assertThat(deserializedDdiDeploymentBase.getDeployment().getUpdate()).isEqualTo(ddiDeployment.getUpdate());
|
||||
assertThat(deserializedDdiDeploymentBase.getDeployment().getMaintenanceWindow()).isEqualTo(
|
||||
ddiDeployment.getMaintenanceWindow());
|
||||
assertThat(deserializedDdiDeploymentBase.getActionHistory().toString()).isEqualTo(ddiActionHistory.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiDeploymentBase = "{\"id\":\"1234\",\"deployment\":{\"download\":\"forced\","
|
||||
+ "\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]},"
|
||||
+ "\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\","
|
||||
+ "\"Action status message 2\"]},\"links\":[],\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiDeploymentBase ddiDeploymentBase = mapper.readValue(serializedDdiDeploymentBase, DdiDeploymentBase.class);
|
||||
|
||||
assertThat(ddiDeploymentBase.getDeployment().getDownload().getName()).isEqualTo(FORCED.getName());
|
||||
assertThat(ddiDeploymentBase.getDeployment().getUpdate().getName()).isEqualTo(ATTEMPT.getName());
|
||||
assertThat(ddiDeploymentBase.getDeployment().getMaintenanceWindow().getStatus()).isEqualTo(
|
||||
AVAILABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiDeploymentBase = "{\"id\":[\"1234\"],\"deployment\":{\"download\":\"forced\","
|
||||
+ "\"update\":\"attempt\",\"maintenanceWindow\":\"available\",\"chunks\":[]},"
|
||||
+ "\"actionHistory\":{\"status\":\"TestAction\",\"messages\":[\"Action status message 1\","
|
||||
+ "\"Action status message 2\"]},\"links\":[]}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiDeploymentBase, DdiDeploymentBase.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus.AVAILABLE;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.ATTEMPT;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType.FORCED;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiDeployment'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiDeploymentTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
DdiDeployment ddiDeployment = new DdiDeployment(FORCED, ATTEMPT, Collections.emptyList(), AVAILABLE);
|
||||
|
||||
// Test
|
||||
String serializedDdiDeployment = mapper.writeValueAsString(ddiDeployment);
|
||||
DdiDeployment deserializedDdiDeployment = mapper.readValue(serializedDdiDeployment, DdiDeployment.class);
|
||||
|
||||
assertThat(serializedDdiDeployment).contains(ddiDeployment.getDownload().getName(),
|
||||
ddiDeployment.getMaintenanceWindow().getStatus());
|
||||
assertThat(deserializedDdiDeployment.getDownload().getName()).isEqualTo(ddiDeployment.getDownload().getName());
|
||||
assertThat(deserializedDdiDeployment.getUpdate().getName()).isEqualTo(ddiDeployment.getUpdate().getName());
|
||||
assertThat(deserializedDdiDeployment.getMaintenanceWindow().getStatus()).isEqualTo(
|
||||
ddiDeployment.getMaintenanceWindow().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiDeployment = "{\"download\":\"forced\",\"update\":\"attempt\", "
|
||||
+ "\"maintenanceWindow\":\"available\",\"chunks\":[],\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiDeployment ddiDeployment = mapper.readValue(serializedDdiDeployment, DdiDeployment.class);
|
||||
|
||||
assertThat(ddiDeployment.getDownload().getName()).isEqualTo(FORCED.getName());
|
||||
assertThat(ddiDeployment.getUpdate().getName()).isEqualTo(ATTEMPT.getName());
|
||||
assertThat(ddiDeployment.getMaintenanceWindow().getStatus()).isEqualTo(AVAILABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiDeployment = "{\"download\":[\"forced\"],\"update\":\"attempt\", "
|
||||
+ "\"maintenanceWindow\":\"available\",\"chunks\":[]}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiDeployment, DdiDeployment.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiMetadata'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiMetadataTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
String key = "testKey";
|
||||
String value = "testValue";
|
||||
DdiMetadata ddiMetadata = new DdiMetadata(key, value);
|
||||
|
||||
// Test
|
||||
String serializedDdiMetadata = mapper.writeValueAsString(ddiMetadata);
|
||||
DdiMetadata deserializedDdiMetadata = mapper.readValue(serializedDdiMetadata, DdiMetadata.class);
|
||||
|
||||
assertThat(serializedDdiMetadata).contains(key, value);
|
||||
assertThat(deserializedDdiMetadata.getKey()).isEqualTo(ddiMetadata.getKey());
|
||||
assertThat(deserializedDdiMetadata.getValue()).isEqualTo(ddiMetadata.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiMetadata = "{\"key\":\"testKey\",\"value\":\"testValue\",\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiMetadata ddiMetadata = mapper.readValue(serializedDdiMetadata, DdiMetadata.class);
|
||||
|
||||
assertThat(ddiMetadata.getKey()).isEqualTo("testKey");
|
||||
assertThat(ddiMetadata.getValue()).isEqualTo("testValue");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiMetadata = "{\"key\":[\"testKey\"],\"value\":\"testValue\"}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiMetadata, DdiMetadata.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiPolling'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiPollingTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
DdiPolling ddiPolling = new DdiPolling("10");
|
||||
|
||||
// Test
|
||||
String serializedDdiPolling = mapper.writeValueAsString(ddiPolling);
|
||||
DdiPolling deserializedDdiPolling = mapper.readValue(serializedDdiPolling, DdiPolling.class);
|
||||
|
||||
assertThat(serializedDdiPolling).contains(ddiPolling.getSleep());
|
||||
assertThat(deserializedDdiPolling.getSleep()).isEqualTo(ddiPolling.getSleep());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiPolling = "{\"sleep\":\"10\",\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiPolling ddiPolling = mapper.readValue(serializedDdiPolling, DdiPolling.class);
|
||||
|
||||
assertThat(ddiPolling.getSleep()).isEqualTo("10");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiPolling = "{\"sleep\":[\"10\"]}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiPolling, DdiPolling.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiProgress'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiProgressTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
DdiProgress ddiProgress = new DdiProgress(30, 100);
|
||||
|
||||
// Test
|
||||
String serializedDdiProgress = mapper.writeValueAsString(ddiProgress);
|
||||
DdiProgress deserializedDdiProgress = mapper.readValue(serializedDdiProgress, DdiProgress.class);
|
||||
|
||||
assertThat(serializedDdiProgress).contains(ddiProgress.getCnt().toString(), ddiProgress.getOf().toString());
|
||||
assertThat(deserializedDdiProgress.getCnt()).isEqualTo(ddiProgress.getCnt());
|
||||
assertThat(deserializedDdiProgress.getOf()).isEqualTo(ddiProgress.getOf());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiProgress = "{\"cnt\":30,\"of\":100,\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiProgress ddiProgress = mapper.readValue(serializedDdiProgress, DdiProgress.class);
|
||||
|
||||
assertThat(ddiProgress.getCnt()).isEqualTo(30);
|
||||
assertThat(ddiProgress.getOf()).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiProgress = "{\"cnt\":[30],\"of\":100}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiProgress, DdiProgress.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiResult.FinalResult.NONE;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiResult'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiResultTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject() throws IOException {
|
||||
// Setup
|
||||
DdiProgress ddiProgress = new DdiProgress(30, 100);
|
||||
DdiResult ddiResult = new DdiResult(NONE, ddiProgress);
|
||||
|
||||
// Test
|
||||
String serializedDdiResult = mapper.writeValueAsString(ddiResult);
|
||||
DdiResult deserializedDdiResult = mapper.readValue(serializedDdiResult, DdiResult.class);
|
||||
|
||||
assertThat(serializedDdiResult).contains(NONE.getName(), ddiProgress.getCnt().toString(),
|
||||
ddiProgress.getOf().toString());
|
||||
assertThat(deserializedDdiResult.getFinished()).isEqualTo(ddiResult.getFinished());
|
||||
assertThat(deserializedDdiResult.getProgress().getCnt()).isEqualTo(ddiProgress.getCnt());
|
||||
assertThat(deserializedDdiResult.getProgress().getOf()).isEqualTo(ddiProgress.getOf());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiResult = "{\"finished\":\"none\",\"progress\":{\"cnt\":30,\"of\":100},\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
DdiResult ddiResult = mapper.readValue(serializedDdiResult, DdiResult.class);
|
||||
|
||||
assertThat(ddiResult.getFinished()).isEqualTo(NONE);
|
||||
assertThat(ddiResult.getProgress().getCnt()).isEqualTo(30);
|
||||
assertThat(ddiResult.getProgress().getOf()).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiResult = "{\"finished\":[\"none\"],\"progress\":{\"cnt\":30,\"of\":100}}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiResult, DdiResult.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiResult.FinalResult.NONE;
|
||||
import static org.eclipse.hawkbit.ddi.json.model.DdiStatus.ExecutionStatus.PROCEEDING;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
/**
|
||||
* Test serializability of DDI api model 'DdiStatus'
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serializability of DDI api Models")
|
||||
public class DdiStatusTest {
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("ddiStatusPossibilities")
|
||||
@Description("Verify the correct serialization and deserialization of the model")
|
||||
public void shouldSerializeAndDeserializeObject(final DdiResult ddiResult, final DdiStatus ddiStatus) throws IOException {
|
||||
// Test
|
||||
String serializedDdiStatus = mapper.writeValueAsString(ddiStatus);
|
||||
DdiStatus deserializedDdiStatus = mapper.readValue(serializedDdiStatus, DdiStatus.class);
|
||||
|
||||
assertThat(serializedDdiStatus).contains(ddiStatus.getExecution().getName(), ddiResult.getFinished().getName(),
|
||||
ddiResult.getProgress().getCnt().toString(), ddiResult.getProgress().getOf().toString());
|
||||
assertThat(deserializedDdiStatus.getExecution()).isEqualTo(ddiStatus.getExecution());
|
||||
assertThat(deserializedDdiStatus.getResult().getFinished()).isEqualTo(ddiStatus.getResult().getFinished());
|
||||
assertThat(deserializedDdiStatus.getResult().getProgress().getCnt()).isEqualTo(
|
||||
ddiStatus.getResult().getProgress().getCnt());
|
||||
assertThat(deserializedDdiStatus.getResult().getProgress().getOf()).isEqualTo(
|
||||
ddiStatus.getResult().getProgress().getOf());
|
||||
assertThat(deserializedDdiStatus.getDetails()).isEqualTo(ddiStatus.getDetails());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a additional unknown property")
|
||||
public void shouldDeserializeObjectWithUnknownProperty() throws IOException {
|
||||
// Setup
|
||||
final String serializedDdiStatus = "{\"execution\":\"proceeding\",\"result\":{\"finished\":\"none\","
|
||||
+ "\"progress\":{\"cnt\":30,\"of\":100}},\"details\":[],\"unknownProperty\":\"test\"}";
|
||||
|
||||
// Test
|
||||
final DdiStatus ddiStatus = mapper.readValue(serializedDdiStatus, DdiStatus.class);
|
||||
|
||||
assertThat(ddiStatus.getExecution()).isEqualTo(PROCEEDING);
|
||||
assertThat(ddiStatus.getCode()).isNull();
|
||||
assertThat(ddiStatus.getResult().getFinished()).isEqualTo(NONE);
|
||||
assertThat(ddiStatus.getResult().getProgress().getCnt()).isEqualTo(30);
|
||||
assertThat(ddiStatus.getResult().getProgress().getOf()).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify the correct deserialization of a model with a provided code (optional)")
|
||||
public void shouldDeserializeObjectWithOptionalCode() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiStatus = "{\"execution\":\"proceeding\",\"result\":{\"finished\":\"none\","
|
||||
+ "\"progress\":{\"cnt\":30,\"of\":100}},\"code\": 12,\"details\":[]}";
|
||||
|
||||
// Test
|
||||
DdiStatus ddiStatus = mapper.readValue(serializedDdiStatus, DdiStatus.class);
|
||||
|
||||
assertThat(ddiStatus.getExecution()).isEqualTo(PROCEEDING);
|
||||
assertThat(ddiStatus.getCode()).isEqualTo(12);
|
||||
assertThat(ddiStatus.getResult().getFinished()).isEqualTo(NONE);
|
||||
assertThat(ddiStatus.getResult().getProgress().getCnt()).isEqualTo(30);
|
||||
assertThat(ddiStatus.getResult().getProgress().getOf()).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that deserialization fails for known properties with a wrong datatype")
|
||||
public void shouldFailForObjectWithWrongDataTypes() throws IOException {
|
||||
// Setup
|
||||
String serializedDdiStatus = "{\"execution\":[\"proceeding\"],\"result\":{\"finished\":\"none\","
|
||||
+ "\"progress\":{\"cnt\":30,\"of\":100}},\"details\":[]}";
|
||||
|
||||
// Test
|
||||
assertThatExceptionOfType(MismatchedInputException.class).isThrownBy(
|
||||
() -> mapper.readValue(serializedDdiStatus, DdiStatus.class));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> ddiStatusPossibilities() {
|
||||
final DdiProgress ddiProgress = new DdiProgress(30, 100);
|
||||
final DdiResult ddiResult = new DdiResult(NONE, ddiProgress);
|
||||
return Stream.of(
|
||||
Arguments.of(ddiResult, new DdiStatus(PROCEEDING, ddiResult, null, Collections.emptyList())),
|
||||
Arguments.of(ddiResult, new DdiStatus(PROCEEDING, ddiResult, null, Collections.singletonList("testMessage"))),
|
||||
Arguments.of(ddiResult, new DdiStatus(PROCEEDING, ddiResult, 12, Collections.emptyList())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.github.classgraph.ClassGraph;
|
||||
import io.github.classgraph.ClassInfo;
|
||||
import io.github.classgraph.ScanResult;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Check DDI api model classes for '@JsonIgnoreProperties' annotation
|
||||
*/
|
||||
@Feature("Unit Tests - Direct Device Integration API")
|
||||
@Story("Serialization of DDI api Models")
|
||||
public class JsonIgnorePropertiesAnnotationTest {
|
||||
|
||||
@Test
|
||||
@Description("This test verifies that all model classes within the 'org.eclipse.hawkbit.ddi.json.model' package are annotated with '@JsonIgnoreProperties(ignoreUnknown = true)'")
|
||||
public void shouldCheckAnnotationsForAllModelClasses() {
|
||||
final String packageName = getClass().getPackage().getName();
|
||||
try (final ScanResult scanResult = new ClassGraph().acceptPackages(packageName).scan()) {
|
||||
final List<? extends Class<?>> matchingClasses = scanResult.getAllClasses()
|
||||
.stream()
|
||||
.filter(classInPackage -> classInPackage.getSimpleName().endsWith("Test") || classInPackage.isEnum())
|
||||
.map(ClassInfo::loadClass)
|
||||
.toList();
|
||||
|
||||
assertThat(matchingClasses).isNotEmpty();
|
||||
matchingClasses.forEach(modelClass -> {
|
||||
if (modelClass.getSimpleName().contains("Test") || modelClass.isEnum()) {
|
||||
return;
|
||||
}
|
||||
final JsonIgnoreProperties annotation = modelClass.getAnnotation(JsonIgnoreProperties.class);
|
||||
assertThat(annotation)
|
||||
.describedAs(
|
||||
"Annotation 'JsonIgnoreProperties' is missing for class: " + modelClass.getSimpleName())
|
||||
.isNotNull();
|
||||
assertThat(annotation.ignoreUnknown()).isTrue();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
14
hawkbit-ddi/hawkbit-ddi-resource/README.md
Normal file
14
hawkbit-ddi/hawkbit-ddi-resource/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Eclipse.IoT hawkBit - DDI Resource
|
||||
|
||||
This is the server-side implementation of the hawkBit DDI API and the hawkBit DDI Download API that is used by devices
|
||||
for communicating with the HawkBit Update Server through HTTP.
|
||||
|
||||
# Compile
|
||||
|
||||
#### Build hawkbit-ddi-resource
|
||||
|
||||
```
|
||||
$ cd hawkbit/hawkbit-ddi-resource
|
||||
$ mvn clean install
|
||||
```
|
||||
|
||||
105
hawkbit-ddi/hawkbit-ddi-resource/pom.xml
Normal file
105
hawkbit-ddi/hawkbit-ddi-resource/pom.xml
Normal file
@@ -0,0 +1,105 @@
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-ddi-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-ddi-resource</artifactId>
|
||||
<name>hawkBit :: REST :: DDI Resources</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-ddi-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-rest-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.plugin</groupId>
|
||||
<artifactId>spring-plugin-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-test</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-jpa</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-rest-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<classifier>tests</classifier>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.el</groupId>
|
||||
<artifactId>javax.el-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-config</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-http-security</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-json</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-aspects</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context-support</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.ApiType;
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandler;
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.URLPlaceholder;
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.URLPlaceholder.SoftwareData;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifactHash;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfig;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiControllerBase;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiMetadata;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiPolling;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||
import org.eclipse.hawkbit.repository.SystemManagement;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
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.rest.json.model.ResponseList;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Utility class for the DDI API.
|
||||
*/
|
||||
public final class DataConversionHelper {
|
||||
|
||||
// utility class, private constructor.
|
||||
private DataConversionHelper() {
|
||||
|
||||
}
|
||||
|
||||
public static DdiConfirmationBase createConfirmationBase(final Target target, final Action activeAction,
|
||||
final DdiAutoConfirmationState autoConfirmationState, final TenantAware tenantAware) {
|
||||
final String controllerId = target.getControllerId();
|
||||
final DdiConfirmationBase confirmationBase = new DdiConfirmationBase(autoConfirmationState);
|
||||
if (autoConfirmationState.isActive()) {
|
||||
confirmationBase.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.deactivateAutoConfirmation(tenantAware.getCurrentTenant(), controllerId))
|
||||
.withRel(DdiRestConstants.AUTO_CONFIRM_DEACTIVATE).expand());
|
||||
} else {
|
||||
confirmationBase.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.activateAutoConfirmation(tenantAware.getCurrentTenant(), controllerId, null))
|
||||
.withRel(DdiRestConstants.AUTO_CONFIRM_ACTIVATE).expand());
|
||||
}
|
||||
if (activeAction != null && activeAction.isWaitingConfirmation()) {
|
||||
confirmationBase.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getConfirmationBaseAction(tenantAware.getCurrentTenant(), controllerId,
|
||||
activeAction.getId(), calculateEtag(activeAction), null))
|
||||
.withRel(DdiRestConstants.CONFIRMATION_BASE).expand());
|
||||
}
|
||||
|
||||
return confirmationBase;
|
||||
}
|
||||
|
||||
public static DdiControllerBase fromTarget(final Target target, final Action installedAction,
|
||||
final Action activeAction, final String defaultControllerPollTime, final TenantAware tenantAware) {
|
||||
final DdiControllerBase result = new DdiControllerBase(
|
||||
new DdiConfig(new DdiPolling(defaultControllerPollTime)));
|
||||
|
||||
if (activeAction != null) {
|
||||
if (activeAction.isWaitingConfirmation()) {
|
||||
result.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder
|
||||
.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getConfirmationBaseAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
activeAction.getId(), calculateEtag(activeAction), null))
|
||||
.withRel(DdiRestConstants.CONFIRMATION_BASE).expand());
|
||||
|
||||
} else if (activeAction.isCancelingOrCanceled()) {
|
||||
result.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getControllerCancelAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
activeAction.getId()))
|
||||
.withRel(DdiRestConstants.CANCEL_ACTION).expand());
|
||||
} else {
|
||||
// we need to add the hashcode here of the actionWithStatus
|
||||
// because the action might
|
||||
// have changed from 'soft' to 'forced' type and we need to
|
||||
// change the payload of the
|
||||
// response because of eTags.
|
||||
result.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder
|
||||
.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getControllerDeploymentBaseAction(tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
activeAction.getId(), calculateEtag(activeAction), null))
|
||||
.withRel(DdiRestConstants.DEPLOYMENT_BASE_ACTION).expand());
|
||||
}
|
||||
}
|
||||
|
||||
if (installedAction != null && !installedAction.isActive()) {
|
||||
result.add(
|
||||
WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.getControllerInstalledAction(tenantAware.getCurrentTenant(),
|
||||
target.getControllerId(), installedAction.getId(), null))
|
||||
.withRel(DdiRestConstants.INSTALLED_BASE_ACTION).expand());
|
||||
}
|
||||
|
||||
if (target.isRequestControllerAttributes()) {
|
||||
result.add(WebMvcLinkBuilder
|
||||
.linkTo(WebMvcLinkBuilder.methodOn(DdiRootController.class, tenantAware.getCurrentTenant())
|
||||
.putConfigData(null, tenantAware.getCurrentTenant(), target.getControllerId()))
|
||||
.withRel(DdiRestConstants.CONFIG_DATA_ACTION).expand());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static List<DdiChunk> createChunks(final Target target, final Action uAction,
|
||||
final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement,
|
||||
final HttpRequest request, final ControllerManagement controllerManagement) {
|
||||
|
||||
final Map<Long, List<SoftwareModuleMetadata>> metadata = controllerManagement
|
||||
.findTargetVisibleMetaDataBySoftwareModuleId(uAction.getDistributionSet().getModules().stream()
|
||||
.map(SoftwareModule::getId).collect(Collectors.toList()));
|
||||
|
||||
return new ResponseList<>(uAction.getDistributionSet().getModules().stream()
|
||||
.map(module -> new DdiChunk(mapChunkLegacyKeys(module.getType().getKey()), module.getVersion(),
|
||||
module.getName(), module.isEncrypted() ? Boolean.TRUE : null,
|
||||
createArtifacts(target, module, artifactUrlHandler, systemManagement, request),
|
||||
mapMetadata(metadata.get(module.getId()))))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
}
|
||||
|
||||
static List<DdiArtifact> createArtifacts(final Target target, final SoftwareModule module,
|
||||
final ArtifactUrlHandler artifactUrlHandler, final SystemManagement systemManagement,
|
||||
final HttpRequest request) {
|
||||
|
||||
return new ResponseList<>(module.getArtifacts().stream()
|
||||
.map(artifact -> createArtifact(target, artifactUrlHandler, artifact, systemManagement, request))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private static List<DdiMetadata> mapMetadata(final List<SoftwareModuleMetadata> metadata) {
|
||||
return CollectionUtils.isEmpty(metadata) ? null
|
||||
: metadata.stream().map(md -> new DdiMetadata(md.getKey(), md.getValue())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static String mapChunkLegacyKeys(final String key) {
|
||||
if ("application".equals(key)) {
|
||||
return "bApp";
|
||||
}
|
||||
if ("runtime".equals(key)) {
|
||||
return "jvm";
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
private static DdiArtifact createArtifact(final Target target, final ArtifactUrlHandler artifactUrlHandler,
|
||||
final Artifact artifact, final SystemManagement systemManagement, final HttpRequest request) {
|
||||
final DdiArtifact file = new DdiArtifact();
|
||||
file.setHashes(new DdiArtifactHash(artifact.getSha1Hash(), artifact.getMd5Hash(), artifact.getSha256Hash()));
|
||||
file.setFilename(artifact.getFilename());
|
||||
file.setSize(artifact.getSize());
|
||||
|
||||
artifactUrlHandler
|
||||
.getUrls(new URLPlaceholder(systemManagement.getTenantMetadata().getTenant(),
|
||||
systemManagement.getTenantMetadata().getId(), target.getControllerId(), target.getId(),
|
||||
new SoftwareData(artifact.getSoftwareModule().getId(), artifact.getFilename(), artifact.getId(),
|
||||
artifact.getSha1Hash())),
|
||||
ApiType.DDI, request.getURI())
|
||||
.forEach(entry -> file.add(Link.of(entry.getRef()).withRel(entry.getRel()).expand()));
|
||||
|
||||
return file;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an etag for the given {@link Action} based on the entities
|
||||
* hashcode and the {@link Action#isHitAutoForceTime(long)} to reflect a
|
||||
* force switch.
|
||||
*
|
||||
* @param action to calculate the etag for
|
||||
* @return the etag
|
||||
*/
|
||||
private static int calculateEtag(final Action action) {
|
||||
final int prime = 31;
|
||||
int result = action.hashCode();
|
||||
int offsetPrime = action.isHitAutoForceTime(System.currentTimeMillis()) ? 1231 : 1237;
|
||||
offsetPrime = action.hasMaintenanceSchedule() && action.isMaintenanceWindowAvailable() ? 1249 : offsetPrime;
|
||||
|
||||
result = prime * result + offsetPrime;
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import org.eclipse.hawkbit.rest.OpenApiConfiguration;
|
||||
import org.eclipse.hawkbit.rest.RestConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
/**
|
||||
* Enable {@link ComponentScan} in the resource package to setup all
|
||||
* {@link Controller} annotated classes and setup the REST-Resources for the
|
||||
* Direct Device Integration API.
|
||||
*/
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@Import({ RestConfiguration.class, OpenApiConfiguration.class })
|
||||
public class DdiApiConfiguration {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Bosch.IO 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.rest.resource;
|
||||
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.eclipse.hawkbit.rest.OpenApiConfiguration;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(
|
||||
value = OpenApiConfiguration.HAWKBIT_SERVER_SWAGGER_ENABLED,
|
||||
havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public class DdiOpenApiConfiguration {
|
||||
|
||||
private static final String DDI_TOKEN_SEC_SCHEME_NAME = "DDI Target/GatewayToken Authentication";
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
value = "hawkbit.server.swagger.ddi.api.group.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public GroupedOpenApi ddiApi() {
|
||||
return GroupedOpenApi
|
||||
.builder()
|
||||
.group("Direct Device Integration API")
|
||||
.pathsToMatch("/{tenant}/controller/**")
|
||||
.addOpenApiCustomizer(openApi ->
|
||||
openApi
|
||||
.addSecurityItem(new SecurityRequirement().addList(DDI_TOKEN_SEC_SCHEME_NAME))
|
||||
.components(
|
||||
openApi
|
||||
.getComponents()
|
||||
.addSecuritySchemes(DDI_TOKEN_SEC_SCHEME_NAME,
|
||||
new SecurityScheme()
|
||||
.name("Authorization")
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.description("Format: (Target|Gateway)Token <token>"))))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,769 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.artifact.repository.urlhandler.ArtifactUrlHandler;
|
||||
import org.eclipse.hawkbit.artifact.repository.model.DbArtifact;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionHistory;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActivateAutoConfirmation;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiArtifact;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAssignedVersion;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAutoConfirmationState;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiCancel;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiCancelActionToStop;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiChunk;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfigData;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationBase;
|
||||
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.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.DdiResult.FinalResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiUpdateMode;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi;
|
||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||
import org.eclipse.hawkbit.repository.ConfirmationManagement;
|
||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
||||
import org.eclipse.hawkbit.repository.EntityFactory;
|
||||
import org.eclipse.hawkbit.repository.RepositoryConstants;
|
||||
import org.eclipse.hawkbit.repository.SystemManagement;
|
||||
import org.eclipse.hawkbit.repository.UpdateMode;
|
||||
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
|
||||
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
|
||||
import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.InvalidConfirmationFeedbackException;
|
||||
import org.eclipse.hawkbit.repository.exception.SoftwareModuleNotAssignedToTargetException;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.rest.util.FileStreamingUtil;
|
||||
import org.eclipse.hawkbit.rest.util.HttpUtil;
|
||||
import org.eclipse.hawkbit.rest.util.RequestResponseContextHolder;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.eclipse.hawkbit.util.IpUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.bus.BusProperties;
|
||||
import org.springframework.cloud.bus.ServiceMatcher;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* The {@link DdiRootController} of the hawkBit server DDI API that is queried
|
||||
* by the hawkBit controller in order to pull {@link Action}s that have to be
|
||||
* fulfilled and report status updates concerning the {@link Action} processing.
|
||||
*
|
||||
* Transactional (read-write) as all queries at least update the last poll time.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
|
||||
public class DdiRootController implements DdiRootControllerRestApi {
|
||||
|
||||
private static final String GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET = "given action ({}) is not assigned to given target ({}).";
|
||||
private static final String FALLBACK_REMARK = "Initiated using the Device Direct Integration API without providing a remark.";
|
||||
|
||||
@Autowired
|
||||
private ConfirmationManagement confirmationManagement;
|
||||
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ServiceMatcher serviceMatcher;
|
||||
|
||||
@Autowired
|
||||
private BusProperties bus;
|
||||
|
||||
@Autowired
|
||||
private ControllerManagement controllerManagement;
|
||||
|
||||
@Autowired
|
||||
private ArtifactManagement artifactManagement;
|
||||
|
||||
@Autowired
|
||||
private HawkbitSecurityProperties securityProperties;
|
||||
|
||||
@Autowired
|
||||
private TenantAware tenantAware;
|
||||
|
||||
@Autowired
|
||||
private SystemManagement systemManagement;
|
||||
|
||||
@Autowired
|
||||
private ArtifactUrlHandler artifactUrlHandler;
|
||||
|
||||
@Autowired
|
||||
private EntityFactory entityFactory;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<List<DdiArtifact>> getSoftwareModulesArtifacts(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("softwareModuleId") final Long softwareModuleId) {
|
||||
log.debug("getSoftwareModulesArtifacts({})", controllerId);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
|
||||
final SoftwareModule softwareModule = controllerManagement.getSoftwareModule(softwareModuleId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId));
|
||||
|
||||
return new ResponseEntity<>(
|
||||
DataConversionHelper.createArtifacts(target, softwareModule, artifactUrlHandler, systemManagement,
|
||||
new ServletServerHttpRequest(RequestResponseContextHolder.getHttpServletRequest())),
|
||||
HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiControllerBase> getControllerBase(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId) {
|
||||
log.debug("getControllerBase({})", controllerId);
|
||||
|
||||
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, IpUtil
|
||||
.getClientIpFromRequest(RequestResponseContextHolder.getHttpServletRequest(), securityProperties));
|
||||
final Action activeAction = controllerManagement.findActiveActionWithHighestWeight(controllerId).orElse(null);
|
||||
|
||||
final Action installedAction = controllerManagement.getInstalledActionByTarget(controllerId).orElse(null);
|
||||
|
||||
checkAndCancelExpiredAction(activeAction);
|
||||
|
||||
// activeAction
|
||||
return new ResponseEntity<>(DataConversionHelper.fromTarget(target, installedAction, activeAction,
|
||||
activeAction == null ? controllerManagement.getPollingTime()
|
||||
: controllerManagement.getPollingTimeForAction(activeAction.getId()),
|
||||
tenantAware), HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<InputStream> downloadArtifact(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("softwareModuleId") final Long softwareModuleId,
|
||||
@PathVariable("fileName") final String fileName) {
|
||||
final ResponseEntity<InputStream> result;
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final SoftwareModule module = controllerManagement.getSoftwareModule(softwareModuleId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId));
|
||||
|
||||
if (checkModule(fileName, module)) {
|
||||
log.warn("Software module with id {} could not be found.", softwareModuleId);
|
||||
result = ResponseEntity.notFound().build();
|
||||
} else {
|
||||
// Artifact presence is ensured in 'checkModule'
|
||||
final Artifact artifact = module.getArtifactByFilename(fileName).orElseThrow(NoSuchElementException::new);
|
||||
final DbArtifact file = artifactManagement
|
||||
.loadArtifactBinary(artifact.getSha1Hash(), module.getId(), module.isEncrypted())
|
||||
.orElseThrow(() -> new ArtifactBinaryNotFoundException(artifact.getSha1Hash()));
|
||||
|
||||
final String ifMatch = RequestResponseContextHolder.getHttpServletRequest().getHeader(HttpHeaders.IF_MATCH);
|
||||
if (ifMatch != null && !HttpUtil.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) {
|
||||
result = new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
|
||||
} else {
|
||||
final ActionStatus action = checkAndLogDownload(RequestResponseContextHolder.getHttpServletRequest(),
|
||||
target, module.getId());
|
||||
|
||||
final Long statusId = action.getId();
|
||||
|
||||
result = FileStreamingUtil.writeFileResponse(file, artifact.getFilename(), artifact.getCreatedAt(),
|
||||
RequestResponseContextHolder.getHttpServletResponse(),
|
||||
RequestResponseContextHolder.getHttpServletRequest(),
|
||||
(length, shippedSinceLastEvent,
|
||||
total) -> eventPublisher.publishEvent(new DownloadProgressEvent(
|
||||
tenantAware.getCurrentTenant(), statusId, shippedSinceLastEvent,
|
||||
serviceMatcher != null ? serviceMatcher.getBusId() : bus.getId())));
|
||||
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Exception squid:S3655 - Optional access is checked in checkModule
|
||||
// subroutine
|
||||
@SuppressWarnings("squid:S3655")
|
||||
public ResponseEntity<Void> downloadArtifactMd5(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("softwareModuleId") final Long softwareModuleId,
|
||||
@PathVariable("fileName") final String fileName) {
|
||||
final Target target = findTarget(controllerId);
|
||||
|
||||
final SoftwareModule module = controllerManagement.getSoftwareModule(softwareModuleId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(SoftwareModule.class, softwareModuleId));
|
||||
|
||||
if (checkModule(fileName, module)) {
|
||||
log.warn("Software module with id {} could not be found.", softwareModuleId);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
final Artifact artifact = module.getArtifactByFilename(fileName)
|
||||
.orElseThrow(() -> new EntityNotFoundException(Artifact.class, fileName));
|
||||
|
||||
checkAndLogDownload(RequestResponseContextHolder.getHttpServletRequest(), target, module.getId());
|
||||
|
||||
try {
|
||||
FileStreamingUtil.writeMD5FileResponse(RequestResponseContextHolder.getHttpServletResponse(),
|
||||
artifact.getMd5Hash(), fileName);
|
||||
} catch (final IOException e) {
|
||||
log.error("Failed to stream MD5 File", e);
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiDeploymentBase> getControllerDeploymentBaseAction(
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") final Long actionId,
|
||||
@RequestParam(value = "c", required = false, defaultValue = "-1") final int resource,
|
||||
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) {
|
||||
log.debug("getControllerDeploymentBaseAction({},{})", controllerId, resource);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
checkAndCancelExpiredAction(action);
|
||||
|
||||
if (!action.isCancelingOrCanceled() && !action.isWaitingConfirmation()) {
|
||||
|
||||
final DdiDeploymentBase base = generateDdiDeploymentBase(target, action, actionHistoryMessageCount);
|
||||
|
||||
log.debug("Found an active UpdateAction for target {}. returning deployment: {}", controllerId, base);
|
||||
|
||||
controllerManagement.registerRetrieved(action.getId(), RepositoryConstants.SERVER_MESSAGE_PREFIX
|
||||
+ "Target retrieved update action and should start now the download.");
|
||||
|
||||
return new ResponseEntity<>(base, HttpStatus.OK);
|
||||
}
|
||||
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> postDeploymentBaseActionFeedback(@Valid @RequestBody final DdiActionFeedback feedback,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId) {
|
||||
log.debug("postDeploymentBaseActionFeedback for target [{},{}]: {}", controllerId, actionId, feedback);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
if (action.isWaitingConfirmation()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (!action.isActive()) {
|
||||
log.warn("Updating action {} with feedback {} not possible since action not active anymore.",
|
||||
action.getId(), feedback.getStatus());
|
||||
return new ResponseEntity<>(HttpStatus.GONE);
|
||||
}
|
||||
|
||||
controllerManagement.addUpdateActionStatus(generateUpdateStatus(feedback, controllerId, actionId));
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> putConfigData(@Valid @RequestBody final DdiConfigData configData,
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId) {
|
||||
|
||||
controllerManagement.updateControllerAttributes(controllerId, configData.getData(), getUpdateMode(configData));
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiCancel> getControllerCancelAction(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId) {
|
||||
log.debug("getControllerCancelAction({})", controllerId);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
if (action.isCancelingOrCanceled()) {
|
||||
final DdiCancel cancel = new DdiCancel(String.valueOf(action.getId()),
|
||||
new DdiCancelActionToStop(String.valueOf(action.getId())));
|
||||
|
||||
log.debug("Found an active CancelAction for target {}. returning cancel: {}", controllerId, cancel);
|
||||
|
||||
controllerManagement.registerRetrieved(action.getId(), RepositoryConstants.SERVER_MESSAGE_PREFIX
|
||||
+ "Target retrieved cancel action and should start now the cancellation.");
|
||||
|
||||
return new ResponseEntity<>(cancel, HttpStatus.OK);
|
||||
}
|
||||
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> postCancelActionFeedback(@Valid @RequestBody final DdiActionFeedback feedback,
|
||||
@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") @NotEmpty final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId) {
|
||||
log.debug("provideCancelActionFeedback for target [{}]: {}", controllerId, feedback);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
controllerManagement
|
||||
.addCancelActionStatus(generateActionCancelStatus(feedback, target, action.getId(), entityFactory));
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiDeploymentBase> getControllerInstalledAction(@PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId, @PathVariable("actionId") final Long actionId,
|
||||
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) {
|
||||
log.debug("getControllerInstalledAction({})", controllerId);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
if (action.isActive() || action.isCancelingOrCanceled()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
final DdiDeploymentBase base = generateDdiDeploymentBase(target, action, actionHistoryMessageCount);
|
||||
|
||||
log.debug("Found an installed UpdateAction for target {}. returning deployment: {}", controllerId, base);
|
||||
return new ResponseEntity<>(base, HttpStatus.OK);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiConfirmationBase> getConfirmationBase(final String tenant, final String controllerId) {
|
||||
log.debug("getConfirmationBase is called [controllerId={}].", controllerId);
|
||||
final Target target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(controllerId, IpUtil
|
||||
.getClientIpFromRequest(RequestResponseContextHolder.getHttpServletRequest(), securityProperties));
|
||||
final Action activeAction = controllerManagement.findActiveActionWithHighestWeight(controllerId).orElse(null);
|
||||
|
||||
final DdiAutoConfirmationState autoConfirmationState = getAutoConfirmationState(controllerId);
|
||||
|
||||
final DdiConfirmationBase confirmationBase = DataConversionHelper.createConfirmationBase(target, activeAction,
|
||||
autoConfirmationState, tenantAware);
|
||||
return new ResponseEntity<>(confirmationBase, HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<DdiConfirmationBaseAction> getConfirmationBaseAction(
|
||||
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") final Long actionId,
|
||||
@RequestParam(value = "c", required = false, defaultValue = "-1") final int resource,
|
||||
@RequestParam(value = "actionHistory", defaultValue = DdiRestConstants.NO_ACTION_HISTORY) final Integer actionHistoryMessageCount) {
|
||||
log.debug("getConfirmationBaseAction({},{})", controllerId, resource);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
checkAndCancelExpiredAction(action);
|
||||
|
||||
if (!action.isCancelingOrCanceled() && action.isWaitingConfirmation()) {
|
||||
|
||||
final DdiConfirmationBaseAction base = generateDdiConfirmationBase(target, action,
|
||||
actionHistoryMessageCount);
|
||||
|
||||
log.debug("Found an active UpdateAction for target {}. Returning confirmation: {}", controllerId, base);
|
||||
|
||||
return new ResponseEntity<>(base, HttpStatus.OK);
|
||||
}
|
||||
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> postConfirmationActionFeedback(
|
||||
@Valid @RequestBody final DdiConfirmationFeedback feedback, @PathVariable("tenant") final String tenant,
|
||||
@PathVariable("controllerId") final String controllerId,
|
||||
@PathVariable("actionId") @NotNull final Long actionId) {
|
||||
log.debug("provideConfirmationActionFeedback with feedback [controllerId={}, actionId={}]: {}", controllerId,
|
||||
actionId, feedback);
|
||||
|
||||
final Target target = findTarget(controllerId);
|
||||
final Action action = findActionForTarget(actionId, target);
|
||||
|
||||
try {
|
||||
|
||||
switch (feedback.getConfirmation()) {
|
||||
case CONFIRMED:
|
||||
log.info("Controller confirmed the action (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getConfirmation());
|
||||
confirmationManagement.confirmAction(actionId, feedback.getCode(), feedback.getDetails());
|
||||
break;
|
||||
case DENIED:
|
||||
default:
|
||||
log.debug("Controller denied the action (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getConfirmation());
|
||||
confirmationManagement.denyAction(actionId, feedback.getCode(), feedback.getDetails());
|
||||
break;
|
||||
}
|
||||
} catch (final InvalidConfirmationFeedbackException e) {
|
||||
if (e.getReason() == InvalidConfirmationFeedbackException.Reason.ACTION_CLOSED) {
|
||||
log.warn("Updating action {} with confirmation {} not possible since action not active anymore.",
|
||||
action.getId(), feedback.getConfirmation());
|
||||
return new ResponseEntity<>(HttpStatus.GONE);
|
||||
} else if (e.getReason() == InvalidConfirmationFeedbackException.Reason.NOT_AWAITING_CONFIRMATION) {
|
||||
log.debug("Action is not waiting for confirmation, deny request.");
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> activateAutoConfirmation(final String tenant, final String controllerId,
|
||||
final DdiActivateAutoConfirmation body) {
|
||||
final String initiator = body == null ? null : body.getInitiator();
|
||||
final String remark = body == null ? FALLBACK_REMARK : body.getRemark();
|
||||
log.debug("Activate auto-confirmation request for device '{}' with payload: [initiator='{}' | remark='{}'",
|
||||
controllerId, initiator, remark);
|
||||
confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> deactivateAutoConfirmation(final String tenant, final String controllerId) {
|
||||
log.debug("Deactivate auto-confirmation request for device ‘{}‘", controllerId);
|
||||
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);
|
||||
}
|
||||
|
||||
private static boolean checkModule(final String fileName, final SoftwareModule module) {
|
||||
return module == null || module.getArtifactByFilename(fileName).isEmpty();
|
||||
}
|
||||
|
||||
private static HandlingType calculateDownloadType(final Action action) {
|
||||
if (action.isDownloadOnly() || action.isForcedOrTimeForced()) {
|
||||
return HandlingType.FORCED;
|
||||
}
|
||||
return HandlingType.ATTEMPT;
|
||||
}
|
||||
|
||||
private static DdiMaintenanceWindowStatus calculateMaintenanceWindow(final Action action) {
|
||||
if (action.hasMaintenanceSchedule()) {
|
||||
return action.isMaintenanceWindowAvailable() ? DdiMaintenanceWindowStatus.AVAILABLE
|
||||
: DdiMaintenanceWindowStatus.UNAVAILABLE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static HandlingType calculateUpdateType(final Action action, final HandlingType downloadType) {
|
||||
if (action.isDownloadOnly()) {
|
||||
return HandlingType.SKIP;
|
||||
} else if (action.hasMaintenanceSchedule()) {
|
||||
return action.isMaintenanceWindowAvailable() ? downloadType : HandlingType.SKIP;
|
||||
}
|
||||
return downloadType;
|
||||
}
|
||||
|
||||
private static void addMessageIfEmpty(final String text, final List<String> messages) {
|
||||
if (messages != null && messages.isEmpty()) {
|
||||
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX + text + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static ActionStatusCreate generateActionCancelStatus(final DdiActionFeedback feedback, final Target target,
|
||||
final Long actionId, final EntityFactory entityFactory) {
|
||||
|
||||
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
|
||||
|
||||
final List<String> messages = new ArrayList<>();
|
||||
final Status status;
|
||||
switch (feedback.getStatus().getExecution()) {
|
||||
case CANCELED:
|
||||
status = handleCaseCancelCanceled(feedback, target, actionId, messages);
|
||||
break;
|
||||
case REJECTED:
|
||||
log.info("Target rejected the cancellation request (actionId: {}, controllerId: {}).", actionId,
|
||||
target.getControllerId());
|
||||
status = Status.CANCEL_REJECTED;
|
||||
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target rejected the cancellation request.");
|
||||
break;
|
||||
case CLOSED:
|
||||
status = handleCancelClosedCase(feedback, messages);
|
||||
break;
|
||||
default:
|
||||
status = Status.RUNNING;
|
||||
break;
|
||||
}
|
||||
|
||||
if (feedback.getStatus().getDetails() != null) {
|
||||
messages.addAll(feedback.getStatus().getDetails());
|
||||
}
|
||||
|
||||
final Integer code = feedback.getStatus().getCode();
|
||||
if (code != null) {
|
||||
actionStatusCreate.code(code);
|
||||
messages.add("Device reported status code: " + code);
|
||||
}
|
||||
|
||||
return actionStatusCreate.status(status).messages(messages);
|
||||
|
||||
}
|
||||
|
||||
private static Status handleCancelClosedCase(final DdiActionFeedback feedback, final List<String> messages) {
|
||||
final Status status;
|
||||
if (feedback.getStatus().getResult().getFinished() == FinalResult.FAILURE) {
|
||||
status = Status.ERROR;
|
||||
addMessageIfEmpty("Target was not able to complete cancellation", messages);
|
||||
} else {
|
||||
status = Status.CANCELED;
|
||||
addMessageIfEmpty("Cancellation confirmed", messages);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private static Status handleCaseCancelCanceled(final DdiActionFeedback feedback, final Target target,
|
||||
final Long actionId, final List<String> messages) {
|
||||
final Status status;
|
||||
log.error(
|
||||
"Target reported cancel for a cancel which is not supported by the server (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, target.getControllerId(), feedback.getStatus().getExecution());
|
||||
status = Status.WARNING;
|
||||
messages.add(RepositoryConstants.SERVER_MESSAGE_PREFIX
|
||||
+ "Target reported cancel for a cancel which is not supported by the server.");
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the update mode from the given update message.
|
||||
*/
|
||||
private static UpdateMode getUpdateMode(final DdiConfigData configData) {
|
||||
final DdiUpdateMode mode = configData.getMode();
|
||||
if (mode != null) {
|
||||
return UpdateMode.valueOf(mode.name());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ActionStatus checkAndLogDownload(final HttpServletRequest request, final Target target, final Long module) {
|
||||
final Action action = controllerManagement
|
||||
.getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(), module)
|
||||
.orElseThrow(() -> new SoftwareModuleNotAssignedToTargetException(module, target.getControllerId()));
|
||||
final String range = request.getHeader("Range");
|
||||
|
||||
final String message;
|
||||
if (range != null) {
|
||||
message = RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target downloads range " + range + " of: "
|
||||
+ request.getRequestURI();
|
||||
} else {
|
||||
message = RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target downloads " + request.getRequestURI();
|
||||
}
|
||||
|
||||
return controllerManagement.addInformationalActionStatus(
|
||||
entityFactory.actionStatus().create(action.getId()).status(Status.DOWNLOAD).message(message));
|
||||
}
|
||||
|
||||
private ActionStatusCreate generateUpdateStatus(final DdiActionFeedback feedback, final String controllerId,
|
||||
final Long actionId) {
|
||||
|
||||
final ActionStatusCreate actionStatusCreate = entityFactory.actionStatus().create(actionId);
|
||||
|
||||
final List<String> messages = new ArrayList<>();
|
||||
|
||||
if (!CollectionUtils.isEmpty(feedback.getStatus().getDetails())) {
|
||||
messages.addAll(feedback.getStatus().getDetails());
|
||||
}
|
||||
|
||||
final Integer code = feedback.getStatus().getCode();
|
||||
if (code != null) {
|
||||
actionStatusCreate.code(code);
|
||||
messages.add("Device reported status code: " + code);
|
||||
}
|
||||
|
||||
final Status status;
|
||||
switch (feedback.getStatus().getExecution()) {
|
||||
case CANCELED:
|
||||
log.debug("Controller confirmed cancel (actionId: {}, controllerId: {}) as we got {} report.", actionId,
|
||||
controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.CANCELED;
|
||||
addMessageIfEmpty("Target confirmed cancellation.", messages);
|
||||
break;
|
||||
case REJECTED:
|
||||
log.info("Controller reported internal error (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.WARNING;
|
||||
addMessageIfEmpty("Target REJECTED update", messages);
|
||||
break;
|
||||
case CLOSED:
|
||||
status = handleClosedCase(feedback, controllerId, actionId, messages);
|
||||
break;
|
||||
case DOWNLOAD:
|
||||
log.debug("Controller confirmed status of download (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.DOWNLOAD;
|
||||
addMessageIfEmpty("Target confirmed download start", messages);
|
||||
break;
|
||||
case DOWNLOADED:
|
||||
log.debug("Controller confirmed download (actionId: {}, controllerId: {}) as we got {} report.", actionId,
|
||||
controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.DOWNLOADED;
|
||||
addMessageIfEmpty("Target confirmed download finished", messages);
|
||||
break;
|
||||
default:
|
||||
status = handleDefaultCase(feedback, controllerId, actionId, messages);
|
||||
break;
|
||||
}
|
||||
|
||||
return actionStatusCreate.status(status).messages(messages);
|
||||
}
|
||||
|
||||
private Status handleDefaultCase(final DdiActionFeedback feedback, final String controllerId, final Long actionId,
|
||||
final List<String> messages) {
|
||||
final Status status;
|
||||
log.debug("Controller reported intermediate status (actionId: {}, controllerId: {}) as we got {} report.",
|
||||
actionId, controllerId, feedback.getStatus().getExecution());
|
||||
status = Status.RUNNING;
|
||||
addMessageIfEmpty("Target reported " + feedback.getStatus().getExecution(), messages);
|
||||
return status;
|
||||
}
|
||||
|
||||
private Status handleClosedCase(final DdiActionFeedback feedback, final String controllerId, final Long actionId,
|
||||
final List<String> messages) {
|
||||
final Status status;
|
||||
log.debug("Controller reported closed (actionId: {}, controllerId: {}) as we got {} report.", actionId,
|
||||
controllerId, feedback.getStatus().getExecution());
|
||||
if (feedback.getStatus().getResult().getFinished() == FinalResult.FAILURE) {
|
||||
status = Status.ERROR;
|
||||
addMessageIfEmpty("Target reported CLOSED with ERROR!", messages);
|
||||
} else {
|
||||
status = Status.FINISHED;
|
||||
addMessageIfEmpty("Target reported CLOSED with OK!", messages);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private Target findTarget(final String controllerId) {
|
||||
return controllerManagement.getByControllerId(controllerId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId));
|
||||
}
|
||||
|
||||
private Action findActionForTarget(final Long actionId, final Target target) {
|
||||
final Action action = controllerManagement.findActionWithDetails(actionId)
|
||||
.orElseThrow(() -> new EntityNotFoundException(Action.class, actionId));
|
||||
return verifyActionBelongsToTarget(action, target);
|
||||
}
|
||||
|
||||
private Action verifyActionBelongsToTarget(final Action action, final Target target) {
|
||||
if (!action.getTarget().getId().equals(target.getId())) {
|
||||
log.debug(GIVEN_ACTION_IS_NOT_ASSIGNED_TO_GIVEN_TARGET, action.getId(), target.getId());
|
||||
throw new EntityNotFoundException(
|
||||
"Not a valid action (" + action.getId() + ") for target: " + target.getControllerId(), null);
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the action has a maintenance schedule defined but is no longer valid,
|
||||
* cancel the action.
|
||||
*
|
||||
* @param action is the {@link Action} to check.
|
||||
*/
|
||||
private void checkAndCancelExpiredAction(final Action action) {
|
||||
if (action != null && action.hasMaintenanceSchedule() && action.isMaintenanceScheduleLapsed()) {
|
||||
try {
|
||||
controllerManagement.cancelAction(action.getId());
|
||||
} catch (final CancelActionNotAllowedException e) {
|
||||
log.info("Cancel action not allowed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DdiDeploymentBase generateDdiDeploymentBase(final Target target, final Action action,
|
||||
final Integer actionHistoryMessageCount) {
|
||||
final DdiActionHistory actionHistory = generateDdiActionHistory(action, actionHistoryMessageCount).orElse(null);
|
||||
final DdiDeployment ddiDeployment = generateDdiDeployment(target, action);
|
||||
return new DdiDeploymentBase(Long.toString(action.getId()), ddiDeployment, actionHistory);
|
||||
}
|
||||
|
||||
private DdiConfirmationBaseAction generateDdiConfirmationBase(final Target target, final Action action,
|
||||
final Integer actionHistoryMessageCount) {
|
||||
final DdiActionHistory actionHistory = generateDdiActionHistory(action, actionHistoryMessageCount).orElse(null);
|
||||
final DdiDeployment ddiDeployment = generateDdiDeployment(target, action);
|
||||
return new DdiConfirmationBaseAction(Long.toString(action.getId()), ddiDeployment, actionHistory);
|
||||
}
|
||||
|
||||
private DdiDeployment generateDdiDeployment(final Target target, final Action action) {
|
||||
final List<DdiChunk> chunks = DataConversionHelper.createChunks(target, action, artifactUrlHandler,
|
||||
systemManagement, new ServletServerHttpRequest(RequestResponseContextHolder.getHttpServletRequest()),
|
||||
controllerManagement);
|
||||
final HandlingType downloadType = calculateDownloadType(action);
|
||||
final HandlingType updateType = calculateUpdateType(action, downloadType);
|
||||
final DdiMaintenanceWindowStatus maintenanceWindow = calculateMaintenanceWindow(action);
|
||||
return new DdiDeployment(downloadType, updateType, chunks, maintenanceWindow);
|
||||
}
|
||||
|
||||
private Optional<DdiActionHistory> generateDdiActionHistory(final Action action,
|
||||
final Integer actionHistoryMessageCount) {
|
||||
final List<String> actionHistoryMsgs = controllerManagement.getActionHistoryMessages(action.getId(),
|
||||
actionHistoryMessageCount == null ? Integer.parseInt(DdiRestConstants.NO_ACTION_HISTORY)
|
||||
: actionHistoryMessageCount);
|
||||
return actionHistoryMsgs.isEmpty() ? Optional.empty()
|
||||
: Optional.of(new DdiActionHistory(action.getStatus().name(), actionHistoryMsgs));
|
||||
}
|
||||
|
||||
private DdiAutoConfirmationState getAutoConfirmationState(final String controllerId) {
|
||||
return confirmationManagement.getStatus(controllerId).map(status -> {
|
||||
final DdiAutoConfirmationState state = DdiAutoConfirmationState.active(status.getActivatedAt());
|
||||
state.setInitiator(status.getInitiator());
|
||||
state.setRemark(status.getRemark());
|
||||
log.trace("Returning state auto-conf state active [initiator='{}' | activatedAt={}] for device {}",
|
||||
controllerId, status.getInitiator(), status.getActivatedAt());
|
||||
return state;
|
||||
}).orElseGet(() -> {
|
||||
log.trace("Returning state auto-conf state disabled for device {}", controllerId);
|
||||
return DdiAutoConfirmationState.disabled();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
|
||||
import com.fasterxml.jackson.dataformat.cbor.CBORGenerator;
|
||||
import com.fasterxml.jackson.dataformat.cbor.CBORParser;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActionFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiAssignedVersion;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiProgress;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.test.TestConfiguration;
|
||||
import org.eclipse.hawkbit.rest.AbstractRestIntegrationTest;
|
||||
import org.eclipse.hawkbit.rest.RestConfiguration;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
@ContextConfiguration(classes = { DdiApiConfiguration.class, RestConfiguration.class,
|
||||
RepositoryApplicationConfiguration.class, TestConfiguration.class })
|
||||
@Import(TestChannelBinderConfiguration.class)
|
||||
@TestPropertySource(locations = "classpath:/ddi-test.properties")
|
||||
public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrationTest {
|
||||
|
||||
public static final int HTTP_PORT = 8080;
|
||||
protected static final String HTTP_LOCALHOST = String.format("http://localhost:%s/", HTTP_PORT);
|
||||
protected static final String CONTROLLER_BASE = "/{tenant}/controller/v1/{controllerId}";
|
||||
|
||||
protected static final String SOFTWARE_MODULE_ARTIFACTS = CONTROLLER_BASE
|
||||
+ "/softwaremodules/{softwareModuleId}/artifacts";
|
||||
protected static final String DEPLOYMENT_BASE = CONTROLLER_BASE + "/deploymentBase/{actionId}";
|
||||
protected static final String DEPLOYMENT_FEEDBACK = DEPLOYMENT_BASE + "/feedback";
|
||||
protected static final String CANCEL_ACTION = CONTROLLER_BASE + "/cancelAction/{actionId}";
|
||||
protected static final String CANCEL_FEEDBACK = CANCEL_ACTION + "/feedback";
|
||||
protected static final String INSTALLED_BASE = CONTROLLER_BASE + "/installedBase/{actionId}";
|
||||
protected static final String INSTALLED_BASE_ROOT = CONTROLLER_BASE + "/installedBase";
|
||||
protected static final String CONFIRMATION_BASE = CONTROLLER_BASE + "/confirmationBase";
|
||||
protected static final String ACTIVATE_AUTO_CONFIRM = CONFIRMATION_BASE + "/activateAutoConfirm";
|
||||
protected static final String DEACTIVATE_AUTO_CONFIRM = CONFIRMATION_BASE + "/deactivateAutoConfirm";
|
||||
protected static final String CONFIRMATION_BASE_ACTION = CONTROLLER_BASE + "/confirmationBase/{actionId}";
|
||||
|
||||
protected static final String CONFIRMATION_FEEDBACK = CONFIRMATION_BASE_ACTION + "/feedback";
|
||||
|
||||
protected static final int ARTIFACT_SIZE = 5 * 1024;
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* Convert JSON to a CBOR equivalent.
|
||||
*
|
||||
* @param json JSON object to convert
|
||||
* @return Equivalent CBOR data
|
||||
* @throws IOException Invalid JSON input
|
||||
*/
|
||||
protected static byte[] jsonToCbor(final String json) throws IOException {
|
||||
final JsonFactory jsonFactory = new JsonFactory();
|
||||
final JsonParser jsonParser = jsonFactory.createParser(json);
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
final CBORFactory cborFactory = new CBORFactory();
|
||||
final CBORGenerator cborGenerator = cborFactory.createGenerator(out);
|
||||
while (jsonParser.nextToken() != null) {
|
||||
cborGenerator.copyCurrentEvent(jsonParser);
|
||||
}
|
||||
cborGenerator.flush();
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert CBOR to JSON equivalent.
|
||||
*
|
||||
* @param input CBOR data to convert
|
||||
* @return Equivalent JSON string
|
||||
* @throws IOException Invalid CBOR input
|
||||
*/
|
||||
protected static String cborToJson(final byte[] input) throws IOException {
|
||||
final CBORFactory cborFactory = new CBORFactory();
|
||||
final CBORParser cborParser = cborFactory.createParser(input);
|
||||
final JsonFactory jsonFactory = new JsonFactory();
|
||||
final StringWriter stringWriter = new StringWriter();
|
||||
final JsonGenerator jsonGenerator = jsonFactory.createGenerator(stringWriter);
|
||||
while (cborParser.nextToken() != null) {
|
||||
jsonGenerator.copyCurrentEvent(cborParser);
|
||||
}
|
||||
jsonGenerator.flush();
|
||||
return stringWriter.toString();
|
||||
}
|
||||
|
||||
protected static ObjectMapper getMapper() {
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
protected ResultActions postDeploymentFeedback(final String controllerId, final Long actionId, final String content,
|
||||
final ResultMatcher statusMatcher) throws Exception {
|
||||
return postDeploymentFeedback(MediaType.APPLICATION_JSON_UTF8, controllerId, actionId, content.getBytes(),
|
||||
statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions putInstalledBase(final String controllerId, final String content,
|
||||
final ResultMatcher statusMatcher) throws Exception {
|
||||
return mvc.perform(put(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
|
||||
.perform(post(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), controllerId, actionId)
|
||||
.content(content).contentType(mediaType).accept(mediaType))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions postCancelFeedback(final String controllerId, final Long actionId, final String content,
|
||||
final ResultMatcher statusMatcher) throws Exception {
|
||||
return postCancelFeedback(MediaType.APPLICATION_JSON_UTF8, controllerId, actionId, content.getBytes(),
|
||||
statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions postCancelFeedback(final MediaType mediaType, final String controllerId,
|
||||
final Long actionId, final byte[] content, final ResultMatcher statusMatcher) throws Exception {
|
||||
return mvc
|
||||
.perform(post(CANCEL_FEEDBACK, tenantAware.getCurrentTenant(), controllerId, actionId).content(content)
|
||||
.contentType(mediaType).accept(mediaType))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher);
|
||||
}
|
||||
|
||||
protected ResultActions performGet(final String url, final MediaType mediaType, final ResultMatcher statusMatcher,
|
||||
final String... values) throws Exception {
|
||||
return mvc.perform(MockMvcRequestBuilders.get(url, values).accept(mediaType)
|
||||
.with(new RequestOnHawkbitDefaultPortPostProcessor()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(statusMatcher)
|
||||
.andExpect(content().contentTypeCompatibleWith(mediaType));
|
||||
}
|
||||
|
||||
protected ResultActions getAndVerifyDeploymentBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
||||
final ResultActions resultActions = performGet(DEPLOYMENT_BASE, mediaType, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
||||
return verifyBasePayload("$.deployment", resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
||||
downloadType, updateType);
|
||||
}
|
||||
|
||||
protected ResultActions getAndVerifyDeploymentBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final Action.ActionType actionType) throws Exception {
|
||||
return getAndVerifyDeploymentBasePayload(controllerId, mediaType, ds, artifact, artifactSignature, actionId,
|
||||
osModuleId, getDownloadAndUploadType(actionType), getDownloadAndUploadType(actionType));
|
||||
}
|
||||
|
||||
protected ResultActions getAndVerifyInstalledBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final Action.ActionType actionType) throws Exception {
|
||||
final ResultActions resultActions = performGet(INSTALLED_BASE, mediaType, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
||||
return verifyBasePayload("$.deployment", resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
||||
getDownloadAndUploadType(actionType), getDownloadAndUploadType(actionType));
|
||||
}
|
||||
|
||||
protected String installedBaseLink(final String controllerId, final String actionId) {
|
||||
return HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/installedBase/" + actionId;
|
||||
}
|
||||
|
||||
protected String deploymentBaseLink(final String controllerId, final String actionId) {
|
||||
return HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/deploymentBase/" + actionId;
|
||||
}
|
||||
|
||||
protected String getJsonRejectedCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.REJECTED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("rejected"));
|
||||
}
|
||||
|
||||
protected String getJsonRejectedDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.REJECTED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("rejected"));
|
||||
}
|
||||
|
||||
protected String getJsonDownloadDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.DOWNLOAD, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("download"));
|
||||
}
|
||||
|
||||
protected String getJsonDownloadedDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.DOWNLOADED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("download"));
|
||||
}
|
||||
|
||||
protected String getJsonCanceledCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.CANCELED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("canceled"));
|
||||
}
|
||||
|
||||
protected String getJsonCanceledDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.CANCELED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("canceled"));
|
||||
}
|
||||
|
||||
protected String getJsonScheduledCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.SCHEDULED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("scheduled"));
|
||||
}
|
||||
|
||||
protected String getJsonScheduledDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.SCHEDULED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("scheduled"));
|
||||
}
|
||||
|
||||
protected String getJsonResumedCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.RESUMED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("resumed"));
|
||||
}
|
||||
|
||||
protected String getJsonResumedDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.RESUMED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("resumed"));
|
||||
}
|
||||
|
||||
protected String getJsonProceedingCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("proceeding"));
|
||||
}
|
||||
|
||||
protected String getJsonProceedingDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("proceeding"));
|
||||
}
|
||||
|
||||
protected String getJsonClosedCancelActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("closed"));
|
||||
}
|
||||
|
||||
protected String getJsonClosedDeploymentActionFeedback() throws JsonProcessingException {
|
||||
return getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("closed"));
|
||||
}
|
||||
|
||||
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus,
|
||||
final DdiResult.FinalResult finalResult) throws JsonProcessingException {
|
||||
return getJsonActionFeedback(executionStatus, finalResult,
|
||||
Collections.singletonList(RandomStringUtils.randomAlphanumeric(1000)));
|
||||
}
|
||||
|
||||
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus, final DdiResult ddiResult,
|
||||
final List<String> messages) throws JsonProcessingException {
|
||||
final DdiStatus ddiStatus = new DdiStatus(executionStatus, ddiResult, null, messages);
|
||||
return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
|
||||
}
|
||||
|
||||
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus,
|
||||
final DdiResult.FinalResult finalResult, final List<String> messages) throws JsonProcessingException {
|
||||
return getJsonActionFeedback(executionStatus, finalResult, null, messages);
|
||||
}
|
||||
|
||||
protected String getJsonActionFeedback(final DdiStatus.ExecutionStatus executionStatus,
|
||||
final DdiResult.FinalResult finalResult, final Integer code, final List<String> messages) throws JsonProcessingException {
|
||||
final DdiStatus ddiStatus = new DdiStatus(executionStatus, new DdiResult(finalResult, new DdiProgress(2, 5)), code,
|
||||
messages);
|
||||
return objectMapper.writeValueAsString(new DdiActionFeedback(Instant.now().toString(), ddiStatus));
|
||||
}
|
||||
|
||||
protected String getJsonConfirmationFeedback(final DdiConfirmationFeedback.Confirmation confirmation,
|
||||
final Integer code, final List<String> messages) throws JsonProcessingException {
|
||||
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 ResultActions getAndVerifyConfirmationBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
||||
final ResultActions resultActions = performGet(CONFIRMATION_BASE_ACTION, mediaType, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId.toString());
|
||||
return verifyBasePayload("$.confirmation", resultActions, controllerId, ds, artifact, artifactSignature, actionId, osModuleId,
|
||||
downloadType, updateType);
|
||||
}
|
||||
|
||||
static void implicitLock(final DistributionSet set) {
|
||||
((JpaDistributionSet) set).setOptLockRevision(set.getOptLockRevision() + 1);
|
||||
}
|
||||
|
||||
private static String getDownloadAndUploadType(final Action.ActionType actionType) {
|
||||
if (Action.ActionType.FORCED.equals(actionType)) {
|
||||
return "forced";
|
||||
}
|
||||
return "attempt";
|
||||
}
|
||||
|
||||
private ResultActions verifyBasePayload(final String prefix, final ResultActions resultActions, final String controllerId,
|
||||
final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId,
|
||||
final Long osModuleId, final String downloadType, final String updateType) throws Exception {
|
||||
return resultActions.andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId))))
|
||||
.andExpect(jsonPath(prefix + ".download", equalTo(downloadType)))
|
||||
.andExpect(jsonPath(prefix + ".update", equalTo(updateType)))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='jvm')].name",
|
||||
contains(ds.findFirstModuleByType(runtimeType).get().getName())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='jvm')].version",
|
||||
contains(ds.findFirstModuleByType(runtimeType).get().getVersion())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].name",
|
||||
contains(ds.findFirstModuleByType(osType).get().getName())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].version",
|
||||
contains(ds.findFirstModuleByType(osType).get().getVersion())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].size", contains(ARTIFACT_SIZE)))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].filename",
|
||||
contains(artifact.getFilename())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.md5",
|
||||
contains(artifact.getMd5Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.sha1",
|
||||
contains(artifact.getSha1Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.sha256",
|
||||
contains(artifact.getSha256Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0]._links.download-http.href",
|
||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename()
|
||||
+ "/download")))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0]._links.md5sum-http.href",
|
||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename()
|
||||
+ "/download.MD5SUM")))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].size", contains(ARTIFACT_SIZE)))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].filename",
|
||||
contains(artifactSignature.getFilename())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.md5",
|
||||
contains(artifactSignature.getMd5Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.sha1",
|
||||
contains(artifactSignature.getSha1Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.sha256",
|
||||
contains(artifactSignature.getSha256Hash())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1]._links.download-http.href",
|
||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename()
|
||||
+ "/download")))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1]._links.md5sum-http.href",
|
||||
contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/" + controllerId
|
||||
+ "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename()
|
||||
+ "/download.MD5SUM")))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].version",
|
||||
contains(ds.findFirstModuleByType(appType).get().getVersion())))
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].metadata").doesNotExist())
|
||||
.andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].name")
|
||||
.value(ds.findFirstModuleByType(appType).get().getName()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.eclipse.hawkbit.ddi.rest.resource.DdiArtifactDownloadTest.DownloadTestConfiguration;
|
||||
import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
|
||||
import org.eclipse.hawkbit.repository.test.util.WithUser;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
/**
|
||||
* Test artifact downloads from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Artifact Download Resource")
|
||||
@SpringBootTest(classes = { DownloadTestConfiguration.class })
|
||||
public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static volatile int downLoadProgress = 0;
|
||||
private static volatile long shippedBytes = 0;
|
||||
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests non allowed requests on the artifact ressource, e.g. invalid URI, wrong if-match, wrong command.")
|
||||
public void invalidRequestsOnArtifactResource() throws Exception {
|
||||
// create target
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final List<Target> targets = Arrays.asList(target);
|
||||
|
||||
// create ds
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
// create artifact
|
||||
final int artifactSize = 5 * 1024;
|
||||
final byte random[] = RandomUtils.nextBytes(artifactSize);
|
||||
final Artifact artifact = artifactManagement.create(new ArtifactUpload(new ByteArrayInputStream(random),
|
||||
ds.findFirstModuleByType(osType).get().getId(), "file1", false, artifactSize));
|
||||
|
||||
assignDistributionSet(ds, targets);
|
||||
|
||||
// no artifact available
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455",
|
||||
target.getControllerId(), getOsModule(ds))).andExpect(status().isNotFound());
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455.MD5SUM",
|
||||
target.getControllerId(), getOsModule(ds))).andExpect(status().isNotFound());
|
||||
|
||||
// SM does not exist
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}",
|
||||
target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound());
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/1234567890/artifacts/{filename}.MD5SUM",
|
||||
target.getControllerId(), artifact.getFilename())).andExpect(status().isNotFound());
|
||||
|
||||
// test now consistent data to test allowed methods
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
|
||||
.header(HttpHeaders.IF_MATCH, artifact.getSha1Hash()))
|
||||
.andExpect(status().isOk());
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// test failed If-match
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
|
||||
.header(HttpHeaders.IF_MATCH, "fsjkhgjfdhg"))
|
||||
.andExpect(status().isPreconditionFailed());
|
||||
|
||||
// test invalid range
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
|
||||
.header("Range", "bytes=1-10,hdsfjksdh"))
|
||||
.andExpect(header().string("Content-Range", "bytes */" + 5 * 1024))
|
||||
.andExpect(status().isRequestedRangeNotSatisfiable());
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename())
|
||||
.header("Range", "bytes=100-10"))
|
||||
.andExpect(header().string("Content-Range", "bytes */" + 5 * 1024))
|
||||
.andExpect(status().isRequestedRangeNotSatisfiable());
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(put("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/{controllerId}/softwaremodules/{smId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true)
|
||||
@Description("Tests valid downloads through the artifact resource by identifying the artifact not by ID but file name.")
|
||||
public void downloadArtifactThroughFileName() throws Exception {
|
||||
downLoadProgress = 1;
|
||||
shippedBytes = 0;
|
||||
assertThat(softwareModuleManagement.findAll(PAGE)).hasSize(0);
|
||||
|
||||
// create target
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final List<Target> targets = Arrays.asList(target);
|
||||
|
||||
// create ds
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
// create artifact
|
||||
final int artifactSize = (int) quotaManagement.getMaxArtifactSize();
|
||||
final byte random[] = RandomUtils.nextBytes(artifactSize);
|
||||
final Artifact artifact = artifactManagement.create(new ArtifactUpload(new ByteArrayInputStream(random),
|
||||
ds.findFirstModuleByType(osType).get().getId(), "file1", false, artifactSize));
|
||||
|
||||
// download fails as artifact is not yet assigned
|
||||
mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
target.getControllerId(), getOsModule(ds), artifact.getFilename())).andExpect(status().isNotFound());
|
||||
|
||||
// now assign and download successful
|
||||
assignDistributionSet(ds, targets);
|
||||
final MvcResult result = mvc.perform(get(
|
||||
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=" + artifact.getFilename()))
|
||||
.andReturn();
|
||||
|
||||
assertTrue(
|
||||
Arrays.equals(result.getResponse().getContentAsByteArray(), random),
|
||||
"The same file that was uploaded is expected when downloaded");
|
||||
|
||||
// download complete
|
||||
assertThat(downLoadProgress).isEqualTo(10);
|
||||
assertThat(shippedBytes).isEqualTo(artifactSize);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests valid MD5SUm file downloads through the artifact resource by identifying the artifact by ID.")
|
||||
public void downloadMd5sumThroughControllerApi() throws Exception {
|
||||
// create target
|
||||
final Target target = testdataFactory.createTarget();
|
||||
|
||||
// create ds
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
// create artifact
|
||||
final int artifactSize = 5 * 1024;
|
||||
final byte random[] = RandomUtils.nextBytes(artifactSize);
|
||||
final Artifact artifact = artifactManagement.create(
|
||||
new ArtifactUpload(new ByteArrayInputStream(random), getOsModule(ds), "file1", false, artifactSize));
|
||||
|
||||
assignDistributionSet(ds, target);
|
||||
|
||||
// download
|
||||
final MvcResult result = mvc.perform(get(
|
||||
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}.MD5SUM",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), artifact.getFilename()))
|
||||
.andExpect(status().isOk()).andExpect(header().string("Content-Disposition",
|
||||
"attachment;filename=" + artifact.getFilename() + ".MD5SUM"))
|
||||
.andReturn();
|
||||
|
||||
assertThat(result.getResponse().getContentAsByteArray())
|
||||
.isEqualTo((artifact.getMd5Hash() + " " + artifact.getFilename()).getBytes(StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUser(principal = TestdataFactory.DEFAULT_CONTROLLER_ID, authorities = "ROLE_CONTROLLER", allSpPermissions = true)
|
||||
@Description("Test various HTTP range requests for artifact download, e.g. chunk download or download resume.")
|
||||
public void rangeDownloadArtifact() throws Exception {
|
||||
// create target
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final List<Target> targets = Arrays.asList(target);
|
||||
|
||||
// create ds
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final int resultLength = (int) quotaManagement.getMaxArtifactSize();
|
||||
|
||||
// create artifact
|
||||
final byte random[] = RandomUtils.nextBytes(resultLength);
|
||||
final Artifact artifact = artifactManagement.create(
|
||||
new ArtifactUpload(new ByteArrayInputStream(random), getOsModule(ds), "file1", false, resultLength));
|
||||
|
||||
assertThat(random.length).isEqualTo(resultLength);
|
||||
|
||||
// now assign and download successful
|
||||
assignDistributionSet(ds, targets);
|
||||
|
||||
final int range = resultLength / 50;
|
||||
|
||||
// full file download with standard range request
|
||||
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
for (int i = 0; i < resultLength / range; i++) {
|
||||
final String rangeString = "" + i * range + "-" + ((i + 1) * range - 1);
|
||||
|
||||
final MvcResult result = mvc.perform(get(
|
||||
"/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1").header("Range",
|
||||
"bytes=" + rangeString))
|
||||
.andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash()))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().longValue("Content-Length", range))
|
||||
.andExpect(header().string("Content-Range", "bytes " + rangeString + "/" + resultLength))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn();
|
||||
|
||||
outputStream.write(result.getResponse().getContentAsByteArray());
|
||||
}
|
||||
|
||||
assertThat(outputStream.toByteArray()).isEqualTo(random);
|
||||
|
||||
// return last 1000 Bytes
|
||||
MvcResult result = mvc.perform(
|
||||
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
|
||||
.header("Range", "bytes=-1000"))
|
||||
.andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash()))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().longValue("Content-Length", 1000))
|
||||
.andExpect(header().string("Content-Range",
|
||||
"bytes " + (resultLength - 1000) + "-" + (resultLength - 1) + "/" + resultLength))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn();
|
||||
|
||||
assertThat(result.getResponse().getContentAsByteArray())
|
||||
.isEqualTo(Arrays.copyOfRange(random, resultLength - 1000, resultLength));
|
||||
|
||||
// skip first 1000 Bytes and return the rest
|
||||
result = mvc.perform(
|
||||
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
|
||||
.header("Range", "bytes=1000-"))
|
||||
.andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash()))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().longValue("Content-Length", resultLength - 1000))
|
||||
.andExpect(header().string("Content-Range",
|
||||
"bytes " + 1000 + "-" + (resultLength - 1) + "/" + resultLength))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn();
|
||||
|
||||
assertThat(result.getResponse().getContentAsByteArray())
|
||||
.isEqualTo(Arrays.copyOfRange(random, 1000, resultLength));
|
||||
|
||||
// Start download from file end fails
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
|
||||
.header("Range", "bytes=" + random.length + "-"))
|
||||
.andExpect(status().isRequestedRangeNotSatisfiable())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().string("Content-Range", "bytes */" + random.length))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1"));
|
||||
|
||||
// multipart download - first 20 bytes in 2 parts
|
||||
result = mvc.perform(
|
||||
get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), "file1")
|
||||
.header("Range", "bytes=0-9,10-19"))
|
||||
.andExpect(status().isPartialContent()).andExpect(header().string("ETag", artifact.getSha1Hash()))
|
||||
.andExpect(content().contentType("multipart/byteranges; boundary=THIS_STRING_SEPARATES_MULTIPART"))
|
||||
.andExpect(header().string("Accept-Ranges", "bytes"))
|
||||
.andExpect(header().string("Last-Modified", dateFormat.format(new Date(artifact.getCreatedAt()))))
|
||||
.andExpect(header().string("Content-Disposition", "attachment;filename=file1")).andReturn();
|
||||
|
||||
outputStream.reset();
|
||||
|
||||
outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART\r\n".getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream.write(("Content-Range: bytes 0-9/" + resultLength + "\r\n").getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream.write(Arrays.copyOfRange(random, 0, 10));
|
||||
outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART\r\n".getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream
|
||||
.write(("Content-Range: bytes 10-19/" + resultLength + "\r\n").getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream.write(Arrays.copyOfRange(random, 10, 20));
|
||||
outputStream.write("\r\n--THIS_STRING_SEPARATES_MULTIPART--".getBytes(StandardCharsets.ISO_8859_1));
|
||||
|
||||
assertThat(result.getResponse().getContentAsByteArray()).isEqualTo(outputStream.toByteArray());
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
public static class DownloadTestConfiguration {
|
||||
|
||||
@Bean
|
||||
public Listener cancelEventHandlerStubBean() {
|
||||
return new Listener();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Listener {
|
||||
|
||||
@EventListener(classes = DownloadProgressEvent.class)
|
||||
public static void listen(final DownloadProgressEvent event) {
|
||||
downLoadProgress++;
|
||||
shippedBytes += event.getShippedBytesSinceLast();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.integration.json.JsonPathUtils;
|
||||
|
||||
/**
|
||||
* Test cancel action from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Cancel Action Resource")
|
||||
class DdiCancelActionTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
ActionStatusRepository actionStatusRepository;
|
||||
|
||||
@Test
|
||||
@Description("Tests that the cancel action resource can be used with CBOR.")
|
||||
void cancelActionCbor() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
testdataFactory.createTarget();
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
|
||||
// check that we can get the cancel action as CBOR
|
||||
final byte[] result = mvc
|
||||
.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId(), tenantAware.getCurrentTenant())
|
||||
.accept(DdiRestConstants.MEDIA_TYPE_CBOR))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR)).andReturn().getResponse()
|
||||
.getContentAsByteArray();
|
||||
assertThat(JsonPathUtils.<String> evaluate(cborToJson(result), "$.id"))
|
||||
.isEqualTo(String.valueOf(cancelAction.getId()));
|
||||
assertThat(JsonPathUtils.<String> evaluate(cborToJson(result), "$.cancelAction.stopId"))
|
||||
.isEqualTo(String.valueOf(actionId));
|
||||
|
||||
// and submit feedback as CBOR
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(jsonToCbor(getJsonProceedingCancelActionFeedback()))
|
||||
.contentType(DdiRestConstants.MEDIA_TYPE_CBOR).accept(DdiRestConstants.MEDIA_TYPE_CBOR))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test of the controller can continue a started update even after a cancel command if it so desires.")
|
||||
void rootRsCancelActionButContinueAnyway() throws Exception {
|
||||
// prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Target savedTarget = testdataFactory.createTarget();
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
implicitLock(ds);
|
||||
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
|
||||
// controller rejects cancellation
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonRejectedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
|
||||
// get update action anyway
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/" + actionId,
|
||||
tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId))))
|
||||
.andExpect(jsonPath("$.deployment.download", equalTo("forced")))
|
||||
.andExpect(jsonPath("$.deployment.update", equalTo("forced")))
|
||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='jvm')].version",
|
||||
contains(ds.findFirstModuleByType(runtimeType).get().getVersion())))
|
||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].version",
|
||||
contains(ds.findFirstModuleByType(osType).get().getVersion())))
|
||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='bApp')].version",
|
||||
contains(ds.findFirstModuleByType(appType).get().getVersion())));
|
||||
|
||||
// and finish it
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/"
|
||||
+ actionId + "/feedback", tenantAware.getCurrentTenant()).content(
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("message")))
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
// check database after test
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(TestdataFactory.DEFAULT_CONTROLLER_ID).get())
|
||||
.isEqualTo(ds);
|
||||
assertThat(deploymentManagement.getInstalledDistributionSet(TestdataFactory.DEFAULT_CONTROLLER_ID).get())
|
||||
.isEqualTo(ds);
|
||||
assertThat(
|
||||
targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getInstallationDate())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test for cancel operation of a update action.")
|
||||
void rootRsCancelAction() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Target savedTarget = testdataFactory.createTarget();
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
final long timeBeforeFirstPoll = System.currentTimeMillis();
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controller}", tenantAware.getCurrentTenant(),
|
||||
TestdataFactory.DEFAULT_CONTROLLER_ID).accept(MediaTypes.HAL_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk()).andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/"
|
||||
+ TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/" + actionId)));
|
||||
final long timeAfterFirstPoll = System.currentTimeMillis() + 1;
|
||||
assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isBetween(timeBeforeFirstPoll, timeAfterFirstPoll);
|
||||
|
||||
// Retrieved is reported
|
||||
|
||||
List<Action> activeActionsByTarget = deploymentManagement
|
||||
.findActiveActionsByTarget(PAGE, savedTarget.getControllerId()).getContent();
|
||||
|
||||
assertThat(activeActionsByTarget).hasSize(1);
|
||||
assertThat(activeActionsByTarget.get(0).getStatus()).isEqualTo(Status.RUNNING);
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
|
||||
activeActionsByTarget = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent();
|
||||
|
||||
// the canceled action should still be active!
|
||||
assertThat(cancelAction.isActive()).isTrue();
|
||||
assertThat(activeActionsByTarget).hasSize(1);
|
||||
assertThat(activeActionsByTarget.get(0).getStatus()).isEqualTo(Status.CANCELING);
|
||||
|
||||
final long timeBefore2ndPoll = System.currentTimeMillis();
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controller}", tenantAware.getCurrentTenant(),
|
||||
TestdataFactory.DEFAULT_CONTROLLER_ID)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.cancelAction.href",
|
||||
equalTo("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/"
|
||||
+ TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + cancelAction.getId())));
|
||||
final long timeAfter2ndPoll = System.currentTimeMillis() + 1;
|
||||
assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isBetween(timeBefore2ndPoll, timeAfter2ndPoll);
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId(), tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(cancelAction.getId()))))
|
||||
.andExpect(jsonPath("$.cancelAction.stopId", equalTo(String.valueOf(actionId))));
|
||||
assertThat(targetManagement.getByControllerID(TestdataFactory.DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
|
||||
// controller confirmed cancelled action, should not be active anymore
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON)
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
activeActionsByTarget = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent();
|
||||
assertThat(activeActionsByTarget).isEmpty();
|
||||
final Action canceledAction = deploymentManagement.findAction(cancelAction.getId()).get();
|
||||
assertThat(canceledAction.isActive()).isFalse();
|
||||
assertThat(canceledAction.getStatus()).isEqualTo(Status.CANCELED);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests various bad requests and if the server handles them as expected.")
|
||||
void badCancelAction() throws Exception {
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/1",
|
||||
tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/1",
|
||||
tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/1",
|
||||
tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(get("/{tenant}/controller/v1/34534543/cancelAction/1", tenantAware.getCurrentTenant())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
createCancelAction("34534543");
|
||||
|
||||
// wrong media type
|
||||
mvc.perform(get("/{tenant}/controller/v1/34534543/cancelAction/1", tenantAware.getCurrentTenant())
|
||||
.accept(MediaType.APPLICATION_ATOM_XML)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotAcceptable());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests the feedback channel of the cancel operation.")
|
||||
void rootRsCancelActionFeedback() throws Exception {
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget();
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
|
||||
// cancel action manually
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonProceedingCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
assertThat(countActionStatusAll()).isEqualTo(3);
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonResumedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
assertThat(countActionStatusAll()).isEqualTo(4);
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonScheduledCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(5);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
// cancellation canceled -> should remove the action from active
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonCanceledCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(6);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
// cancellation rejected -> action still active until controller close
|
||||
// it
|
||||
// with finished or
|
||||
// error
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonRejectedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(7);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
// update closed -> should remove the action from active
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(8);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests the feeback chanel of for multiple open cancel operations on the same target.")
|
||||
void multipleCancelActionFeedback() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final DistributionSet ds3 = testdataFactory.createDistributionSet("3", true);
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget();
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds);
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds2);
|
||||
final Long actionId3 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds3.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds3);
|
||||
|
||||
assertThat(countActionStatusAll()).isEqualTo(3);
|
||||
|
||||
// 3 update actions, 0 cancel actions
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(3);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(3);
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
final Action cancelAction2 = deploymentManagement.cancelAction(actionId2);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(3);
|
||||
assertThat(deploymentManagement.countActionsByTarget(savedTarget.getControllerId())).isEqualTo(3);
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId(), tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(cancelAction.getId()))))
|
||||
.andExpect(jsonPath("$.cancelAction.stopId", equalTo(String.valueOf(actionId))));
|
||||
assertThat(countActionStatusAll()).isEqualTo(6);
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controllerId}", tenantAware.getCurrentTenant(),
|
||||
TestdataFactory.DEFAULT_CONTROLLER_ID)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.cancelAction.href",
|
||||
equalTo("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/"
|
||||
+ TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + cancelAction.getId())));
|
||||
|
||||
// now lets return feedback for the first cancelation
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(7);
|
||||
|
||||
// 1 update actions, 1 cancel actions
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsByTarget(savedTarget.getControllerId())).isEqualTo(3);
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction2.getId(), tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(cancelAction2.getId()))))
|
||||
.andExpect(jsonPath("$.cancelAction.stopId", equalTo(String.valueOf(actionId2))));
|
||||
assertThat(countActionStatusAll()).isEqualTo(8);
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/{controller}", tenantAware.getCurrentTenant(),
|
||||
TestdataFactory.DEFAULT_CONTROLLER_ID)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.cancelAction.href",
|
||||
equalTo("http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/"
|
||||
+ TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/" + cancelAction2.getId())));
|
||||
|
||||
// now lets return feedback for the second cancelation
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction2.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(9);
|
||||
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(TestdataFactory.DEFAULT_CONTROLLER_ID).get())
|
||||
.isEqualTo(ds3);
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/deploymentBase/" + actionId3,
|
||||
tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(10);
|
||||
|
||||
// 1 update actions, 0 cancel actions
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action cancelAction3 = deploymentManagement.cancelAction(actionId3);
|
||||
|
||||
// action is in cancelling state
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
assertThat(deploymentManagement.countActionsByTarget(savedTarget.getControllerId())).isEqualTo(3);
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(TestdataFactory.DEFAULT_CONTROLLER_ID).get())
|
||||
.isEqualTo(ds3);
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction3.getId(), tenantAware.getCurrentTenant()).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.id", equalTo(String.valueOf(cancelAction3.getId()))))
|
||||
.andExpect(jsonPath("$.cancelAction.stopId", equalTo(String.valueOf(actionId3))));
|
||||
assertThat(countActionStatusAll()).isEqualTo(12);
|
||||
|
||||
// now lets return feedback for the third cancelation
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction3.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(countActionStatusAll()).isEqualTo(13);
|
||||
|
||||
// final status
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
assertThat(deploymentManagement.countActionsByTarget(savedTarget.getControllerId())).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests the feeback channel closing for too many feedbacks, i.e. denial of service prevention.")
|
||||
void tooMuchCancelActionFeedback() throws Exception {
|
||||
testdataFactory.createTarget();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID));
|
||||
|
||||
final Action cancelAction = deploymentManagement.cancelAction(actionId);
|
||||
|
||||
final String feedback = getJsonProceedingCancelActionFeedback();
|
||||
// assignDistributionSet creates an ActionStatus and cancel action
|
||||
// stores an action status, so
|
||||
// only 97 action status left
|
||||
for (int i = 0; i < 98; i++) {
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content(feedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content(feedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("test the correct rejection of various invalid feedback requests")
|
||||
void badCancelActionFeedback() throws Exception {
|
||||
final Action cancelAction = createCancelAction(TestdataFactory.DEFAULT_CONTROLLER_ID);
|
||||
createCancelAction("4715");
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(put("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// bad content type
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_ATOM_XML)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isUnsupportedMediaType());
|
||||
|
||||
// bad body
|
||||
String invalidFeedback = "{\"status\":{\"execution\":\"546456456\",\"result\":{\"finished\":\"none\",\"progress\":{\"cnt\":2,\"of\":5}},\"details\":\"none\"]}}";
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content(invalidFeedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(post("/{tenant}/controller/v1/12345/cancelAction/" + cancelAction.getId() + "/feedback",
|
||||
tenantAware.getCurrentTenant()).content(getJsonClosedCancelActionFeedback())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// invalid action
|
||||
invalidFeedback = "{\"id\":\"sdfsdfsdfs\",\"status\":{\"execution\":\"closed\",\"result\":{\"finished\":\"none\",\"progress\":{\"cnt\":2,\"of\":5}},\"details\":\"details\"]}}";
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content(invalidFeedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest());
|
||||
|
||||
// finaly, get it right :)
|
||||
mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
|
||||
+ cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant())
|
||||
.content(getJsonClosedCancelActionFeedback()).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
private Action createCancelAction(final String targetid) {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet(targetid);
|
||||
final Target savedTarget = testdataFactory.createTarget(targetid);
|
||||
final List<Target> toAssign = new ArrayList<>();
|
||||
toAssign.add(savedTarget);
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, toAssign));
|
||||
|
||||
return deploymentManagement.cancelAction(actionId);
|
||||
}
|
||||
|
||||
private long countActionStatusAll() {
|
||||
return actionStatusRepository.count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.eclipse.hawkbit.repository.test.util.TargetTestData.ATTRIBUTE_KEY_TOO_LONG;
|
||||
import static org.eclipse.hawkbit.repository.test.util.TargetTestData.ATTRIBUTE_KEY_VALID;
|
||||
import static org.eclipse.hawkbit.repository.test.util.TargetTestData.ATTRIBUTE_VALUE_TOO_LONG;
|
||||
import static org.eclipse.hawkbit.repository.test.util.TargetTestData.ATTRIBUTE_VALUE_VALID;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Step;
|
||||
import io.qameta.allure.Story;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.exception.SpServerError;
|
||||
import org.eclipse.hawkbit.repository.exception.AssignmentQuotaExceededException;
|
||||
import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.rest.util.JsonBuilder;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
/**
|
||||
* Test config data from the controller.
|
||||
*/
|
||||
@ActiveProfiles({ "im", "test" })
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Config Data Resource")
|
||||
class DdiConfigDataTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
public static final String TARGET1_ID = "4717";
|
||||
public static final String TARGET1_CONFIGDATA_PATH = "/{tenant}/controller/v1/" + TARGET1_ID + "/configData";
|
||||
public static final String TARGET2_ID = "4718";
|
||||
public static final String TARGET2_CONFIGDATA_PATH = "/{tenant}/controller/v1/" + TARGET2_ID + "/configData";
|
||||
|
||||
@Test
|
||||
@Description("Verify that config data can be uploaded as CBOR")
|
||||
void putConfigDataAsCbor() throws Exception {
|
||||
testdataFactory.createTarget(TARGET1_ID);
|
||||
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put(ATTRIBUTE_KEY_VALID, ATTRIBUTE_VALUE_VALID);
|
||||
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(jsonToCbor(JsonBuilder.configData(attributes).toString()))
|
||||
.contentType(DdiRestConstants.MEDIA_TYPE_CBOR)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
assertThat(targetManagement.getControllerAttributes(TARGET1_ID)).isEqualTo(attributes);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) "
|
||||
+ "are requested only once from the device.")
|
||||
@SuppressWarnings("squid:S2925")
|
||||
void requestConfigDataIfEmpty() throws Exception {
|
||||
final Target savedTarget = testdataFactory.createTarget("4712");
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant()).accept(MediaTypes.HAL_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.configData.href", equalTo(
|
||||
"http://localhost/" + tenantAware.getCurrentTenant() + "/controller/v1/4712/configData")));
|
||||
Thread.sleep(1); // is required: otherwise processing the next line is
|
||||
// often too fast and
|
||||
// the following assert will fail
|
||||
assertThat(targetManagement.getByControllerID("4712").orElseThrow(NoSuchElementException::new)
|
||||
.getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(targetManagement.getByControllerID("4712").orElseThrow(NoSuchElementException::new)
|
||||
.getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
|
||||
final Map<String, String> attributes = new HashMap<>(1);
|
||||
attributes.put("dsafsdf", "sdsds");
|
||||
|
||||
final Target updateControllerAttributes = controllerManagement
|
||||
.updateControllerAttributes(savedTarget.getControllerId(), attributes, null);
|
||||
// request controller attributes need to be false because we don't want
|
||||
// to request the controller attributes again
|
||||
assertThat(updateControllerAttributes.isRequestControllerAttributes()).isFalse();
|
||||
|
||||
mvc.perform(
|
||||
get("/{tenant}/controller/v1/4712", tenantAware.getCurrentTenant()).accept(MediaTypes.HAL_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.configData.href").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) "
|
||||
+ "can be uploaded correctly by the controller.")
|
||||
void putConfigData() throws Exception {
|
||||
testdataFactory.createTarget(TARGET1_ID);
|
||||
|
||||
// initial
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put(ATTRIBUTE_KEY_VALID, ATTRIBUTE_VALUE_VALID);
|
||||
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(targetManagement.getControllerAttributes(TARGET1_ID)).isEqualTo(attributes);
|
||||
|
||||
// update
|
||||
attributes.put("sdsds", "123412");
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
assertThat(targetManagement.getControllerAttributes(TARGET1_ID)).isEqualTo(attributes);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) "
|
||||
+ "upload quota is enforced to protect the server from malicious attempts.")
|
||||
void putTooMuchConfigData() throws Exception {
|
||||
testdataFactory.createTarget(TARGET1_ID);
|
||||
|
||||
// initial
|
||||
Map<String, String> attributes = new HashMap<>();
|
||||
for (int i = 0; i < quotaManagement.getMaxAttributeEntriesPerTarget(); i++) {
|
||||
attributes.put("dsafsdf" + i, "sdsds" + i);
|
||||
}
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
attributes = new HashMap<>();
|
||||
attributes.put("on too many", "sdsds");
|
||||
mvc.perform(put(TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(AssignmentQuotaExceededException.class.getName())))
|
||||
.andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey())));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) "
|
||||
+ "resource behaves as expected in case of invalid request attempts.")
|
||||
void badConfigData() throws Exception {
|
||||
testdataFactory.createTarget("4712");
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).//
|
||||
andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(get("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// bad content type
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put("dsafsdf", "sdsds");
|
||||
mvc.perform(put("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaTypes.HAL_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isUnsupportedMediaType());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(put("/{tenant}/controller/v1/456456/configData", tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// bad body
|
||||
mvc.perform(put("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant())
|
||||
.content("{\"id\": \"51659181\"}").contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that invalid config data attributes are handled correctly.")
|
||||
void putConfigDataWithInvalidAttributes() throws Exception {
|
||||
// create a target
|
||||
testdataFactory.createTarget(TARGET2_ID);
|
||||
putAndVerifyConfigDataWithKeyTooLong();
|
||||
putAndVerifyConfigDataWithValueTooLong();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that config data (device attributes) can be updated by the controller using different update modes (merge, replace, remove).")
|
||||
void putConfigDataWithDifferentUpdateModes() throws Exception {
|
||||
|
||||
// create a target
|
||||
testdataFactory.createTarget(TARGET1_ID);
|
||||
|
||||
// no update mode
|
||||
putConfigDataWithoutUpdateMode();
|
||||
|
||||
// update mode REPLACE
|
||||
putConfigDataWithUpdateModeReplace();
|
||||
|
||||
// update mode MERGE
|
||||
putConfigDataWithUpdateModeMerge();
|
||||
|
||||
// update mode REMOVE
|
||||
putConfigDataWithUpdateModeRemove();
|
||||
|
||||
// invalid update mode
|
||||
putConfigDataWithInvalidUpdateMode();
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putAndVerifyConfigDataWithKeyTooLong() throws Exception {
|
||||
final Map<String, String> attributes = Collections.singletonMap(ATTRIBUTE_KEY_TOO_LONG, ATTRIBUTE_VALUE_VALID);
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET2_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(InvalidTargetAttributeException.class.getName())))
|
||||
.andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_TARGET_ATTRIBUTES_INVALID.getKey())));
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putAndVerifyConfigDataWithValueTooLong() throws Exception {
|
||||
final Map<String, String> attributes = Collections.singletonMap(ATTRIBUTE_KEY_VALID, ATTRIBUTE_VALUE_TOO_LONG);
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET2_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(InvalidTargetAttributeException.class.getName())))
|
||||
.andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_TARGET_ATTRIBUTES_INVALID.getKey())));
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithInvalidUpdateMode() throws Exception {
|
||||
// create some attriutes
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put("k0", "v0");
|
||||
attributes.put("k1", "v1");
|
||||
|
||||
// use an invalid update mode
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes, "KJHGKJHGKJHG").toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithUpdateModeRemove() throws Exception {
|
||||
// get the current attributes
|
||||
final int previousSize = targetManagement.getControllerAttributes(DdiConfigDataTest.TARGET1_ID).size();
|
||||
|
||||
// update the attributes using update mode REMOVE
|
||||
final Map<String, String> removeAttributes = new HashMap<>();
|
||||
removeAttributes.put("k1", "foo");
|
||||
removeAttributes.put("k3", "bar");
|
||||
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(removeAttributes, "remove").toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// verify attribute removal
|
||||
final Map<String, String> updatedAttributes = targetManagement
|
||||
.getControllerAttributes(DdiConfigDataTest.TARGET1_ID);
|
||||
assertThat(updatedAttributes).hasSize(previousSize - 2);
|
||||
assertThat(updatedAttributes).doesNotContainKeys("k1", "k3");
|
||||
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithUpdateModeMerge()
|
||||
throws Exception {
|
||||
// get the current attributes
|
||||
final Map<String, String> attributes = new HashMap<>(
|
||||
targetManagement.getControllerAttributes(DdiConfigDataTest.TARGET1_ID));
|
||||
|
||||
// update the attributes using update mode MERGE
|
||||
final Map<String, String> mergeAttributes = new HashMap<>();
|
||||
mergeAttributes.put("k1", "v1_modified_again");
|
||||
mergeAttributes.put("k4", "v4");
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(mergeAttributes, "merge").toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// verify attribute merge
|
||||
final Map<String, String> updatedAttributes = targetManagement
|
||||
.getControllerAttributes(DdiConfigDataTest.TARGET1_ID);
|
||||
assertThat(updatedAttributes).hasSize(4);
|
||||
assertThat(updatedAttributes).containsAllEntriesOf(mergeAttributes);
|
||||
assertThat(updatedAttributes).containsEntry("k1", "v1_modified_again");
|
||||
attributes.keySet().forEach(assertThat(updatedAttributes)::containsKey);
|
||||
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithUpdateModeReplace()
|
||||
throws Exception {
|
||||
// get the current attributes
|
||||
final Map<String, String> attributes = new HashMap<>(
|
||||
targetManagement.getControllerAttributes(DdiConfigDataTest.TARGET1_ID));
|
||||
|
||||
// update the attributes using update mode REPLACE
|
||||
final Map<String, String> replacementAttributes = new HashMap<>();
|
||||
replacementAttributes.put("k1", "v1_modified");
|
||||
replacementAttributes.put("k2", "v2");
|
||||
replacementAttributes.put("k3", "v3");
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(replacementAttributes, "replace").toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk());
|
||||
|
||||
// verify attribute replacement
|
||||
final Map<String, String> updatedAttributes = targetManagement
|
||||
.getControllerAttributes(DdiConfigDataTest.TARGET1_ID);
|
||||
assertThat(updatedAttributes).hasSize(replacementAttributes.size());
|
||||
assertThat(updatedAttributes).containsAllEntriesOf(replacementAttributes);
|
||||
assertThat(updatedAttributes).containsEntry("k1", "v1_modified");
|
||||
attributes.entrySet().forEach(assertThat(updatedAttributes)::doesNotContain);
|
||||
|
||||
}
|
||||
|
||||
@Step
|
||||
private void putConfigDataWithoutUpdateMode()
|
||||
throws Exception {
|
||||
// create some attriutes
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put("k0", "v0");
|
||||
attributes.put("k1", "v1");
|
||||
|
||||
// set the initial attributes
|
||||
mvc.perform(put(DdiConfigDataTest.TARGET1_CONFIGDATA_PATH, tenantAware.getCurrentTenant())
|
||||
.content(JsonBuilder.configData(attributes).toString()).contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
// verify the initial parameters
|
||||
final Map<String, String> updatedAttributes = targetManagement
|
||||
.getControllerAttributes(DdiConfigDataTest.TARGET1_ID);
|
||||
assertThat(updatedAttributes).containsExactlyEntriesOf(attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO 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.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.eclipse.hawkbit.repository.jpa.management.JpaConfirmationManagement.CONFIRMATION_CODE_MSG_PREFIX;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Step;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiActivateAutoConfirmation;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent;
|
||||
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;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
/**
|
||||
* Test confirmation base from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Confirmation Action Resource")
|
||||
class DdiConfirmationBaseTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static final String DEFAULT_CONTROLLER_ID = "4747";
|
||||
|
||||
@Test
|
||||
@Description("Forced deployment to a controller. Checks if the confirmation resource response payload for a given deployment is as expected.")
|
||||
void verifyConfirmationReferencesInControllerBase(@Autowired ActionStatusRepository actionStatusRepository) throws Exception {
|
||||
enableConfirmationFlow();
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget(DdiConfirmationBaseTest.DEFAULT_CONTROLLER_ID);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
|
||||
final List<Target> targetsAssignedToDs = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
|
||||
Action.ActionType.FORCED).getAssignedEntity().stream().map(Action::getTarget)
|
||||
.collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
|
||||
assignDistributionSet(ds2, targetsAssignedToDs).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
final long current = System.currentTimeMillis();
|
||||
final String expectedConfirmationBaseLink = String.format("/%s/controller/v1/%s/confirmationBase/%d",
|
||||
tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, uaction.getId());
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href",
|
||||
containsString(expectedConfirmationBaseLink)))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(actionStatusRepository.count()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyConfirmationBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact,
|
||||
artifactSignature, action.getId(),
|
||||
findDistributionSetByAction.findFirstModuleByType(osType).get().getId(), "forced", "forced");
|
||||
|
||||
// Retrieved is reported
|
||||
final Iterable<ActionStatus> actionStatus = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Sort.Direction.DESC, "id"), uaction.getId());
|
||||
assertThat(actionStatus).hasSize(1)
|
||||
.allMatch(status -> status.getStatus() == Action.Status.WAIT_FOR_CONFIRMATION);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the deployment resource is available as CBOR")
|
||||
void confirmationResourceCbor() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet distributionSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
assignDistributionSet(distributionSet.getId(), target.getName());
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
// get confirmation base
|
||||
performGet(CONFIRMATION_BASE_ACTION, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(), action.getId().toString());
|
||||
|
||||
// get artifacts
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
String.valueOf(softwareModuleId));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the confirmation endpoint is not available.")
|
||||
void confirmationEndpointNotExposed() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("988");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
final String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href").doesNotExist());
|
||||
|
||||
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the deploymentBase endpoint is not available for action ins WFC state.")
|
||||
void deploymentEndpointNotAccessibleForActionsWFC() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("988");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
final String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href").exists())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the confirmation endpoints are still available after deactivating the confirmation flow.")
|
||||
void verifyConfirmationBaseEndpointsArePresentAfterDisablingConfirmationFlow() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("988");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
final String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
// disable confirmation flow
|
||||
disableConfirmationFlow();
|
||||
|
||||
// confirmation base should still be exposed
|
||||
verifyActionInConfirmationBaseState(savedTarget.getControllerId(), savedAction.getId());
|
||||
|
||||
// verify confirmation endpoint is still accessible
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.DENIED, 20,
|
||||
"Action denied message.").andExpect(status().isOk());
|
||||
|
||||
// confirmation base should still be exposed
|
||||
verifyActionInConfirmationBaseState(savedTarget.getControllerId(), savedAction.getId());
|
||||
|
||||
// verify confirmation endpoint is still accessible
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.CONFIRMED, 10,
|
||||
"Action confirmed message.").andExpect(status().isOk());
|
||||
|
||||
// assert deployment link is exposed to the target
|
||||
verifyActionInDeploymentBaseState(controllerId, savedAction.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Controller sends a confirmed action state.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1),
|
||||
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||
void sendConfirmedActionStateFeedbackTest() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("988");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.CONFIRMED, 10,
|
||||
"Action confirmed message.").andExpect(status().isOk());
|
||||
|
||||
// assert deployment link is exposed to the target
|
||||
verifyActionInDeploymentBaseState(controllerId, savedAction.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Confirmation base provides right values if auto-confirm not active.")
|
||||
void getConfirmationBaseProvidesAutoConfirmStatusNotActive() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final String controllerId = testdataFactory.createTarget("989").getControllerId();
|
||||
assignDistributionSet(testdataFactory.createDistributionSet("").getId(), controllerId);
|
||||
final long actionId = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent().get(0)
|
||||
.getId();
|
||||
|
||||
final String confirmationBaseActionLink = String.format("/%s/controller/v1/%s/confirmationBase/%d",
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId);
|
||||
|
||||
final String activateAutoConfLink = String.format("/%s/controller/v1/%s/confirmationBase/activateAutoConfirm",
|
||||
tenantAware.getCurrentTenant(), controllerId);
|
||||
|
||||
mvc.perform(
|
||||
get(CONFIRMATION_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("autoConfirm.active", equalTo(Boolean.FALSE)))
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href", containsString(confirmationBaseActionLink)))
|
||||
.andExpect(jsonPath("$._links.activateAutoConfirm.href", containsString(activateAutoConfLink)))
|
||||
.andExpect(jsonPath("$._links.deactivateAutoConfirm").doesNotExist());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("possibleActiveStates")
|
||||
@Description("Confirmation base provides right values if auto-confirm is active.")
|
||||
void getConfirmationBaseProvidesAutoConfirmStatusActive(final String initiator, final String remark)
|
||||
throws Exception {
|
||||
final String controllerId = testdataFactory.createTarget("988").getControllerId();
|
||||
|
||||
confirmationManagement.activateAutoConfirmation(controllerId, initiator, remark);
|
||||
|
||||
final String deactivateAutoConfLink = String.format(
|
||||
"/%s/controller/v1/%s/confirmationBase/deactivateAutoConfirm", tenantAware.getCurrentTenant(),
|
||||
controllerId);
|
||||
|
||||
mvc.perform(
|
||||
get(CONFIRMATION_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("autoConfirm.active", equalTo(Boolean.TRUE)))
|
||||
.andExpect(initiator == null ? jsonPath("autoConfirm.initiator").doesNotExist()
|
||||
: jsonPath("autoConfirm.initiator", equalTo(initiator)))
|
||||
.andExpect(remark == null ? jsonPath("autoConfirm.remark").doesNotExist()
|
||||
: jsonPath("autoConfirm.remark", equalTo(remark)))
|
||||
.andExpect(jsonPath("$._links.deactivateAutoConfirm.href", containsString(deactivateAutoConfLink)))
|
||||
.andExpect(jsonPath("$._links.activateAutoConfirm").doesNotExist());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("possibleActiveStates")
|
||||
@Description("Verify auto-confirm activation is handled correctly.")
|
||||
void activateAutoConfirmation(final String initiator, final String remark) throws Exception {
|
||||
final String controllerId = testdataFactory.createTarget("988").getControllerId();
|
||||
|
||||
final DdiActivateAutoConfirmation body = new DdiActivateAutoConfirmation(initiator, remark);
|
||||
|
||||
mvc.perform(post(ACTIVATE_AUTO_CONFIRM, tenantAware.getCurrentTenant(), controllerId)
|
||||
.content(getMapper().writeValueAsString(body)).contentType(MediaType.APPLICATION_JSON_UTF8))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
assertThat(confirmationManagement.getStatus(controllerId)).hasValueSatisfying(status -> {
|
||||
assertThat(status.getInitiator()).isEqualTo(initiator);
|
||||
assertThat(status.getRemark()).isEqualTo(remark);
|
||||
assertThat(status.getCreatedBy()).isEqualTo("bumlux");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify auto-confirm deactivation is handled correctly.")
|
||||
void deactivateAutoConfirmation() throws Exception {
|
||||
final String controllerId = testdataFactory.createTarget("988").getControllerId();
|
||||
|
||||
confirmationManagement.activateAutoConfirmation(controllerId, null, null);
|
||||
|
||||
mvc.perform(post(DEACTIVATE_AUTO_CONFIRM, tenantAware.getCurrentTenant(), controllerId))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
assertThat(confirmationManagement.getStatus(controllerId)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Controller sends a denied action state.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1),
|
||||
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||
void sendDeniedActionStateFeedbackTest() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("989");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
String controllerId = savedTarget.getControllerId();
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.DENIED, 10,
|
||||
"Action denied message.").andExpect(status().isOk());
|
||||
|
||||
// asserts that deployment link is not available
|
||||
final String expectedConfirmationBaseLink = String.format("/%s/controller/v1/%s/confirmationBase/%d",
|
||||
tenantAware.getCurrentTenant(), controllerId, savedAction.getId());
|
||||
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href", containsString(expectedConfirmationBaseLink)));
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), controllerId, savedAction.getId())
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerDeploymentActionFeedback endpoint.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||
void testActionHistoryCount() throws Exception {
|
||||
enableConfirmationFlow();
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("990");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
|
||||
String controllerId = savedTarget.getControllerId();
|
||||
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, controllerId).getContent()
|
||||
.get(0);
|
||||
final String CONFIRMED_MESSAGE = "Action confirmed message.";
|
||||
final Integer CONFIRMED_CODE = 10;
|
||||
sendConfirmationFeedback(savedTarget, savedAction, DdiConfirmationFeedback.Confirmation.CONFIRMED,
|
||||
CONFIRMED_CODE, CONFIRMED_MESSAGE).andExpect(status().isOk());
|
||||
|
||||
// confirmationBase not available in RUNNING state anymore
|
||||
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), savedTarget.getControllerId(),
|
||||
savedAction.getId()).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// assert confirmed message against deploymentBase endpoint
|
||||
// this call will update the action due to retrieved action status update
|
||||
mvc.perform(
|
||||
get(DEPLOYMENT_BASE + "?actionHistory=2", tenantAware.getCurrentTenant(), savedTarget.getControllerId(),
|
||||
savedAction.getId()).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString(CONFIRMED_MESSAGE))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages",
|
||||
hasItem(containsString(String.format(CONFIRMATION_CODE_MSG_PREFIX, CONFIRMED_CODE)))));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> possibleActiveStates() {
|
||||
return Stream.of(Arguments.of("someInitiator", "someRemark"), Arguments.of(null, "someRemark"),
|
||||
Arguments.of("someInitiator", null), Arguments.of(null, null));
|
||||
}
|
||||
|
||||
@Step
|
||||
private void verifyActionInDeploymentBaseState(final String controllerId, final long actionId) throws Exception {
|
||||
final String expectedDeploymentBaseLink = String.format("/%s/controller/v1/%s/deploymentBase/%d",
|
||||
tenantAware.getCurrentTenant(), controllerId, actionId);
|
||||
|
||||
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)))
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href").doesNotExist());
|
||||
|
||||
// assert that deployment endpoint is working
|
||||
mvc.perform(get(expectedDeploymentBaseLink).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Step
|
||||
private void verifyActionInConfirmationBaseState(final String controllerId, final long actionId) throws Exception {
|
||||
mvc.perform(
|
||||
get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.confirmationBase.href").exists())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
mvc.perform(get(CONFIRMATION_BASE_ACTION, tenantAware.getCurrentTenant(), controllerId, actionId)
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
mvc.perform(get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), controllerId, actionId)
|
||||
.accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
private ResultActions sendConfirmationFeedback(final Target target, final Action action,
|
||||
final DdiConfirmationFeedback.Confirmation confirmation, Integer code, String message) throws Exception {
|
||||
|
||||
if (message == null) {
|
||||
message = RandomStringUtils.randomAlphanumeric(1000);
|
||||
}
|
||||
|
||||
final String feedback = getJsonConfirmationFeedback(confirmation, code, Collections.singletonList(message));
|
||||
return mvc.perform(
|
||||
post(CONFIRMATION_FEEDBACK, tenantAware.getCurrentTenant(), target.getControllerId(), action.getId())
|
||||
.content(feedback).contentType(MediaType.APPLICATION_JSON));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,838 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.jpa.JpaManagementHelper;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.ActionType;
|
||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||
import org.eclipse.hawkbit.rest.exception.MessageNotReadableException;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
/**
|
||||
* Test deployment base from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Deployment Action Resource")
|
||||
class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static final String DEFAULT_CONTROLLER_ID = "4712";
|
||||
|
||||
@Autowired
|
||||
private ActionRepository actionRepository;
|
||||
@Autowired
|
||||
private ActionStatusRepository actionStatusRepository;
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the deployment resource is available as CBOR")
|
||||
void deploymentResourceCbor() throws Exception {
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet distributionSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
assignDistributionSet(distributionSet.getId(), target.getName());
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
// get deployment base
|
||||
performGet(DEPLOYMENT_BASE, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), action.getId().toString());
|
||||
|
||||
// get artifacts
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
String.valueOf(softwareModuleId));
|
||||
|
||||
final byte[] feedback = jsonToCbor(getJsonProceedingDeploymentActionFeedback());
|
||||
|
||||
postDeploymentFeedback(MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), target.getControllerId(),
|
||||
action.getId(), feedback, status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that artifacts are not found, when software module does not exists.")
|
||||
void artifactsNotFound() throws Exception {
|
||||
final Target target = testdataFactory.createTarget();
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.APPLICATION_JSON, status().isNotFound(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), "1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that artifacts are found, when software module exists.")
|
||||
void artifactsExists() throws Exception {
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet distributionSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
assignDistributionSet(distributionSet.getId(), target.getName());
|
||||
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.APPLICATION_JSON, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), softwareModuleId.toString())
|
||||
.andExpect(jsonPath("$", hasSize(3)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename0')]", hasSize(1)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename1')]", hasSize(1)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename2')]", hasSize(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Forced deployment to a controller. Checks if the resource response payload for a given deployment is as expected.")
|
||||
void deploymentForceAction() throws Exception {
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
final Target savedTarget = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final List<Target> targetsAssignedToDs = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
|
||||
ActionType.FORCED).getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
|
||||
assignDistributionSet(ds2, targetsAssignedToDs).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
final long current = System.currentTimeMillis();
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString()))));
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact,
|
||||
artifactSignature, action.getId(),
|
||||
findDistributionSetByAction.findFirstModuleByType(osType).get().getId(), "forced", "forced");
|
||||
|
||||
// Retrieved is reported
|
||||
final Iterable<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Direction.DESC, "id"), uaction.getId());
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Checks that the deploymentBase URL changes when the action is switched from soft to forced in TIMEFORCED case.")
|
||||
void changeEtagIfActionSwitchesFromSoftToForced() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds.getId(), target.getControllerId(),
|
||||
ActionType.TIMEFORCED, System.currentTimeMillis() + 2_000));
|
||||
|
||||
MvcResult mvcResult = performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID).andReturn();
|
||||
|
||||
final String urlBeforeSwitch = JsonPath.compile("_links.deploymentBase.href")
|
||||
.read(mvcResult.getResponse().getContentAsString()).toString();
|
||||
|
||||
// Time is not yet over, so we should see the same URL
|
||||
mvcResult = performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andReturn();
|
||||
assertThat(JsonPath.compile("_links.deploymentBase.href").read(mvcResult.getResponse().getContentAsString())
|
||||
.toString()).isEqualTo(urlBeforeSwitch)
|
||||
.startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, actionId.toString()));
|
||||
|
||||
// After the time is over we should see a new etag
|
||||
TimeUnit.MILLISECONDS.sleep(2_000);
|
||||
|
||||
mvcResult = performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andReturn();
|
||||
|
||||
assertThat(JsonPath.compile("_links.deploymentBase.href").read(mvcResult.getResponse().getContentAsString())
|
||||
.toString()).isNotEqualTo(urlBeforeSwitch);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Attempt/soft deployment to a controller. Checks if the resource response payload for a given deployment is as expected.")
|
||||
void deploymentAttemptAction() throws Exception {
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final String visibleMetadataOsKey = "metaDataVisible";
|
||||
final String visibleMetadataOsValue = "withValue";
|
||||
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(getOsModule(ds))
|
||||
.key(visibleMetadataOsKey).value(visibleMetadataOsValue).targetVisible(true));
|
||||
softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(getOsModule(ds))
|
||||
.key("metaDataNotVisible").value("withValue").targetVisible(false));
|
||||
|
||||
final Target savedTarget = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final List<Target> saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(), ActionType.SOFT)
|
||||
.getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
assignDistributionSet(ds2, saved).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString()))));
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, visibleMetadataOsKey,
|
||||
visibleMetadataOsValue, artifact, artifactSignature, action.getId(), "attempt", "attempt",
|
||||
getOsModule(findDistributionSetByAction));
|
||||
|
||||
// Retrieved is reported
|
||||
final List<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Direction.DESC, "id"), uaction.getId()).getContent();
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Attempt/soft deployment to a controller including automated switch to hard. Checks if the resource response payload for a given deployment is as expected.")
|
||||
void deploymentAutoForceAction() throws Exception {
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final Target savedTarget = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final List<Target> saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
|
||||
ActionType.TIMEFORCED).getAssignedEntity().stream().map(Action::getTarget).collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
assignDistributionSet(ds2, saved).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString()))));
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact,
|
||||
artifactSignature, action.getId(),
|
||||
findDistributionSetByAction.findFirstModuleByType(osType).get().getId(), "forced", "forced");
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaTypes.HAL_JSON, ds, artifact, artifactSignature,
|
||||
action.getId(), findDistributionSetByAction.findFirstModuleByType(osType).get().getId(), "forced",
|
||||
"forced");
|
||||
|
||||
// Retrieved is reported
|
||||
final Iterable<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Direction.DESC, "id"), uaction.getId()).getContent();
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test download-only (forced + skip) deployment to a controller. Checks if the resource response payload for a given deployment is as expected.")
|
||||
void deploymentDownloadOnlyAction() throws Exception {
|
||||
// Prepare test data
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(getOsModule(ds))
|
||||
.key("metaDataVisible").value("withValue").targetVisible(true));
|
||||
softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(getOsModule(ds))
|
||||
.key("metaDataNotVisible").value("withValue").targetVisible(false));
|
||||
|
||||
final Target savedTarget = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final List<Target> saved = assignDistributionSet(ds.getId(), savedTarget.getControllerId(),
|
||||
ActionType.DOWNLOAD_ONLY).getAssignedEntity().stream().map(Action::getTarget)
|
||||
.collect(Collectors.toList());
|
||||
implicitLock(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(1);
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(1);
|
||||
assignDistributionSet(ds2, saved).getAssignedEntity();
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
assertThat(deploymentManagement.countActionsAll()).isEqualTo(2);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
assertThat(uaction.getDistributionSet()).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).hasSize(2);
|
||||
|
||||
// Run test
|
||||
|
||||
final long current = System.currentTimeMillis();
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID).andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(DEFAULT_CONTROLLER_ID, uaction.getId().toString()))));
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getLastTargetQuery())
|
||||
.isLessThanOrEqualTo(System.currentTimeMillis());
|
||||
assertThat(countActionStatusAll()).isEqualTo(2);
|
||||
|
||||
final DistributionSet findDistributionSetByAction = distributionSetManagement.getByAction(action.getId()).get();
|
||||
|
||||
getAndVerifyDeploymentBasePayload(DEFAULT_CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, "metaDataVisible",
|
||||
"withValue", artifact, artifactSignature, action.getId(), "forced", "skip",
|
||||
getOsModule(findDistributionSetByAction));
|
||||
|
||||
// Retrieved is reported
|
||||
final List<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Direction.DESC, "id"), uaction.getId()).getContent();
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Status.RETRIEVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test various invalid access attempts to the deployment resource und the expected behaviour of the server.")
|
||||
void badDeploymentAction() throws Exception {
|
||||
final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), "not-existing", "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// no deployment
|
||||
mvc.perform(
|
||||
MockMvcRequestBuilders.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// wrong media type
|
||||
final List<Target> toAssign = Collections.singletonList(target);
|
||||
final DistributionSet savedSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(savedSet, toAssign));
|
||||
mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID,
|
||||
actionId)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
mvc.perform(MockMvcRequestBuilders
|
||||
.get(DEPLOYMENT_BASE, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, actionId)
|
||||
.accept(MediaType.APPLICATION_ATOM_XML)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotAcceptable());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("The server protects itself against to many feedback upload attempts. The test verifies that "
|
||||
+ "it is not possible to exceed the configured maximum number of feedback uploads.")
|
||||
void tooMuchDeploymentActionFeedback() throws Exception {
|
||||
final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
assignDistributionSet(ds.getId(), DEFAULT_CONTROLLER_ID);
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
|
||||
final String feedback = getJsonProceedingDeploymentActionFeedback();
|
||||
// assign distribution set creates an action status, so only 99 left
|
||||
for (int i = 0; i < 99; i++) {
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isOk());
|
||||
}
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("The server protects itself against too large feedback bodies. The test verifies that "
|
||||
+ "it is not possible to exceed the configured maximum number of feedback details.")
|
||||
void tooMuchDeploymentActionMessagesInFeedback() throws Exception {
|
||||
final Target target = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
assignDistributionSet(ds.getId(), DEFAULT_CONTROLLER_ID);
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
|
||||
final List<String> messages = new ArrayList<>();
|
||||
for (int i = 0; i < quotaManagement.getMaxMessagesPerActionStatus() + 1; i++) {
|
||||
messages.add(String.valueOf(i));
|
||||
}
|
||||
|
||||
final String feedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE,
|
||||
null, messages);
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(), feedback, status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Multiple uploads of deployment status feedback to the server.")
|
||||
void multipleDeploymentActionFeedback() throws Exception {
|
||||
testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
testdataFactory.createTarget("4713");
|
||||
testdataFactory.createTarget("4714");
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final DistributionSet ds3 = testdataFactory.createDistributionSet("3", true);
|
||||
|
||||
final Long actionId1 = getFirstAssignedActionId(assignDistributionSet(ds1.getId(), DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds1);
|
||||
final Long actionId2 = getFirstAssignedActionId(assignDistributionSet(ds2.getId(), DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds2);
|
||||
final Long actionId3 = getFirstAssignedActionId(assignDistributionSet(ds3.getId(), DEFAULT_CONTROLLER_ID));
|
||||
implicitLock(ds3);
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.PENDING, 3, Optional.empty());
|
||||
assertThat(targetManagement.findByUpdateStatus(PageRequest.of(0, 10), TargetUpdateStatus.UNKNOWN)).hasSize(2);
|
||||
|
||||
// action1 done
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId1, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.PENDING, 2,
|
||||
Optional.of(ds1));
|
||||
assertStatusMessagesCount(4);
|
||||
|
||||
// action2 done
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId2, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.PENDING, 1,
|
||||
Optional.of(ds2));
|
||||
assertStatusMessagesCount(5);
|
||||
|
||||
// action3 done
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId3, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds3), TargetUpdateStatus.IN_SYNC, 0,
|
||||
Optional.of(ds3));
|
||||
assertStatusMessagesCount(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that an update action is correctly set to error if the controller provides error feedback.")
|
||||
void rootRsSingleDeploymentActionWithErrorFeedback() throws Exception {
|
||||
DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Target savedTarget = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
|
||||
assertThat(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get().getUpdateStatus())
|
||||
.isEqualTo(TargetUpdateStatus.UNKNOWN);
|
||||
assignDistributionSet(ds, Collections.singletonList(savedTarget));
|
||||
final Action action = actionRepository
|
||||
.findAll(ActionSpecifications.byDistributionSetId(ds.getId()), PAGE)
|
||||
.map(Action.class::cast).getContent().get(0);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action.getId(),
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.FAILURE,
|
||||
Collections.singletonList("Error message")),
|
||||
status().isOk());
|
||||
|
||||
findTargetAndAssertUpdateStatus(Optional.empty(), TargetUpdateStatus.ERROR, 0, Optional.empty());
|
||||
assertThat(deploymentManagement.countActionsByTarget(DEFAULT_CONTROLLER_ID)).isEqualTo(1);
|
||||
assertTargetCountByStatus(0, 1, 0);
|
||||
|
||||
// redo
|
||||
ds = distributionSetManagement.getWithDetails(ds.getId()).get();
|
||||
assignDistributionSet(ds,
|
||||
Collections.singletonList(targetManagement.getByControllerID(DEFAULT_CONTROLLER_ID).get()));
|
||||
final Action action2 = deploymentManagement.findActiveActionsByTarget(PAGE, DEFAULT_CONTROLLER_ID).getContent()
|
||||
.get(0);
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, action2.getId(), getJsonClosedCancelActionFeedback(),
|
||||
status().isOk());
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds), TargetUpdateStatus.IN_SYNC, 0, Optional.of(ds));
|
||||
assertTargetCountByStatus(0, 0, 1);
|
||||
assertThat(deploymentManagement.findInActiveActionsByTarget(PAGE, DEFAULT_CONTROLLER_ID)).hasSize(2);
|
||||
assertThat(countActionStatusAll()).isEqualTo(4);
|
||||
assertThat(deploymentManagement.findActionStatusByAction(PAGE, action.getId()).getContent()).haveAtLeast(1,
|
||||
new ActionStatusCondition(Status.ERROR));
|
||||
assertThat(deploymentManagement.findActionStatusByAction(PAGE, action2.getId()).getContent()).haveAtLeast(1,
|
||||
new ActionStatusCondition(Status.FINISHED));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that the controller can provided as much feedback entries as necessary as long as it is in the configured limits.")
|
||||
void rootRsSingleDeploymentActionFeedback() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds,
|
||||
Collections.singletonList(testdataFactory.createTarget(DEFAULT_CONTROLLER_ID))));
|
||||
implicitLock(ds);
|
||||
findTargetAndAssertUpdateStatus(Optional.of(ds), TargetUpdateStatus.PENDING, 1, Optional.empty());
|
||||
|
||||
// Now valid Feedback
|
||||
for (int i = 0; i < 4; i++) {
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonProceedingDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertActionStatusCount(i + 2, i);
|
||||
|
||||
}
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonScheduledDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertActionStatusCount(6, 5);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonResumedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertActionStatusCount(7, 6);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonCanceledDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertStatusAndActiveActionsCount(TargetUpdateStatus.PENDING, 1);
|
||||
assertActionStatusCount(8, 7, 0, 0, 1);
|
||||
assertTargetCountByStatus(1, 0, 0);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonRejectedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertStatusAndActiveActionsCount(TargetUpdateStatus.PENDING, 1);
|
||||
assertActionStatusCount(9, 6, 1, 0, 1);
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, actionId, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
assertStatusAndActiveActionsCount(TargetUpdateStatus.IN_SYNC, 0);
|
||||
assertActionStatusCount(10, 7, 1, 1, 1);
|
||||
assertTargetCountByStatus(0, 0, 1);
|
||||
|
||||
assertThat(targetManagement.findByInstalledDistributionSet(PAGE, ds.getId())).hasSize(1);
|
||||
assertThat(targetManagement.findByAssignedDistributionSet(PAGE, ds.getId())).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Various forbidden request attempts on the feedback resource. Ensures correct answering behaviour as expected to these kind of errors.")
|
||||
void badDeploymentActionFeedback() throws Exception {
|
||||
final DistributionSet savedSet = testdataFactory.createDistributionSet("");
|
||||
final DistributionSet savedSet2 = testdataFactory.createDistributionSet("1");
|
||||
|
||||
// target does not exist
|
||||
|
||||
postDeploymentFeedback(DEFAULT_CONTROLLER_ID, 1234L, getJsonProceedingDeploymentActionFeedback(),
|
||||
status().isNotFound());
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget(DEFAULT_CONTROLLER_ID);
|
||||
|
||||
// Action does not exist
|
||||
postDeploymentFeedback("4713", 1234L, getJsonProceedingDeploymentActionFeedback(), status().isNotFound());
|
||||
|
||||
assignDistributionSet(savedSet, Collections.singletonList(savedTarget)).getAssignedEntity().iterator().next();
|
||||
assignDistributionSet(savedSet2, Collections.singletonList(testdataFactory.createTarget("4713")));
|
||||
|
||||
final Action updateAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
// action exists but is not assigned to this target
|
||||
postDeploymentFeedback("4713", updateAction.getId(), getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING,
|
||||
DdiResult.FinalResult.NONE, Collections.singletonList("")), status().isNotFound());
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(MockMvcRequestBuilders.get(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(),
|
||||
DEFAULT_CONTROLLER_ID, "2")).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "2"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete(DEPLOYMENT_FEEDBACK, tenantAware.getCurrentTenant(), DEFAULT_CONTROLLER_ID, "2"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that an invalid id in feedback body returns a bad request.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = TargetUpdatedEvent.class, count = 1) })
|
||||
void invalidIdInFeedbackReturnsBadRequest() throws Exception {
|
||||
final Target target = testdataFactory.createTarget("1080");
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
assignDistributionSet(ds.getId(), "1080");
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
final String invalidFeedback = "{\"id\":\"AAAA\",\"status\":{\"execution\":\"proceeding\",\"result\":{\"finished\":\"none\",\"progress\":{\"cnt\":2,\"of\":5}},\"details\":\"details\"]}}";
|
||||
postDeploymentFeedback("1080", action.getId(), invalidFeedback, status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a missing feedback result in feedback body returns a bad request.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = TargetUpdatedEvent.class, count = 1) })
|
||||
void missingResultAttributeInFeedbackReturnsBadRequest() throws Exception {
|
||||
|
||||
final Target target = testdataFactory.createTarget("1080");
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
assignDistributionSet(ds.getId(), "1080");
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
|
||||
final String missingResultInFeedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, (DdiResult) null,
|
||||
Collections.singletonList("test"));
|
||||
postDeploymentFeedback("1080", action.getId(), missingResultInFeedback, status().isBadRequest())
|
||||
.andExpect(jsonPath("$.*", hasSize(3)))
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(MessageNotReadableException.class.getName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a missing finished result in feedback body returns a bad request.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = TargetUpdatedEvent.class, count = 1) })
|
||||
void missingFinishedAttributeInFeedbackReturnsBadRequest() throws Exception {
|
||||
final Target target = testdataFactory.createTarget("1080");
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
assignDistributionSet(ds.getId(), "1080");
|
||||
|
||||
final Action action = deploymentManagement.findActionsByTarget(target.getControllerId(), PAGE).getContent()
|
||||
.get(0);
|
||||
final String missingFinishedResultInFeedback = getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED,
|
||||
new DdiResult(null, null),
|
||||
Collections.singletonList("test"));
|
||||
|
||||
postDeploymentFeedback("1080", action.getId(), missingFinishedResultInFeedback, status().isBadRequest())
|
||||
.andExpect(jsonPath("$.*", hasSize(3)))
|
||||
.andExpect(jsonPath("$.exceptionClass", equalTo(MessageNotReadableException.class.getName())));
|
||||
}
|
||||
|
||||
private long countActionStatusAll() {
|
||||
return actionStatusRepository.count();
|
||||
}
|
||||
|
||||
private void getAndVerifyDeploymentBasePayload(final String controllerId, final MediaType mediaType,
|
||||
final DistributionSet ds, final String visibleMetadataOsKey, final String visibleMetadataOsValue,
|
||||
final Artifact artifact, final Artifact artifactSignature, final Long actionId, final String downloadType,
|
||||
final String updateType, final Long osModuleId) throws Exception {
|
||||
getAndVerifyDeploymentBasePayload(controllerId, mediaType, ds, artifact, artifactSignature, actionId,
|
||||
osModuleId, downloadType, updateType).andExpect(
|
||||
jsonPath("$.deployment.chunks[?(@.part=='os')].metadata[0].key").value(visibleMetadataOsKey))
|
||||
.andExpect(jsonPath("$.deployment.chunks[?(@.part=='os')].metadata[0].value")
|
||||
.value(visibleMetadataOsValue));
|
||||
}
|
||||
|
||||
private void assertActionStatusCount(final int actionStatusCount, final int minActionStatusCountInPage) {
|
||||
final Target target = targetManagement.getByControllerID(DdiDeploymentBaseTest.DEFAULT_CONTROLLER_ID).get();
|
||||
assertThat(target.getUpdateStatus()).isEqualTo(TargetUpdateStatus.PENDING);
|
||||
|
||||
assertTargetCountByStatus(1, 0, 0);
|
||||
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())).hasSize(1);
|
||||
assertThat(countActionStatusAll()).isEqualTo(actionStatusCount);
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(minActionStatusCountInPage,
|
||||
new ActionStatusCondition(Status.RUNNING));
|
||||
}
|
||||
|
||||
private Target createTargetAndAssertNoActiveActions() {
|
||||
final Target savedTarget = testdataFactory.createTarget(DdiDeploymentBaseTest.DEFAULT_CONTROLLER_ID);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
assertThat(deploymentManagement.countActionsAll()).isZero();
|
||||
assertThat(countActionStatusAll()).isZero();
|
||||
return savedTarget;
|
||||
}
|
||||
|
||||
private void assertStatusMessagesCount(final int actionStatusMessagesCount) {
|
||||
final Iterable<ActionStatus> actionStatusMessages;
|
||||
actionStatusMessages = findActionStatusAll(PageRequest.of(0, 100, Direction.DESC, "id"))
|
||||
.getContent();
|
||||
assertThat(actionStatusMessages).hasSize(actionStatusMessagesCount);
|
||||
assertThat(actionStatusMessages).haveAtLeast(1, new ActionStatusCondition(Status.FINISHED));
|
||||
}
|
||||
|
||||
private void findTargetAndAssertUpdateStatus(final Optional<DistributionSet> ds,
|
||||
final TargetUpdateStatus updateStatus, final int activeActions,
|
||||
final Optional<DistributionSet> installedDs) {
|
||||
final Target myT = targetManagement.getByControllerID(DdiDeploymentBaseTest.DEFAULT_CONTROLLER_ID).get();
|
||||
assertThat(myT.getUpdateStatus()).isEqualTo(updateStatus);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, myT.getControllerId())).hasSize(activeActions);
|
||||
assertThat(deploymentManagement.getAssignedDistributionSet(myT.getControllerId())).isEqualTo(ds);
|
||||
assertThat(deploymentManagement.getInstalledDistributionSet(myT.getControllerId())).isEqualTo(installedDs);
|
||||
}
|
||||
|
||||
private void assertTargetCountByStatus(final int pending, final int error, final int inSync) {
|
||||
assertThat(targetManagement.findByUpdateStatus(PageRequest.of(0, 10), TargetUpdateStatus.PENDING))
|
||||
.hasSize(pending);
|
||||
assertThat(targetManagement.findByUpdateStatus(PageRequest.of(0, 10), TargetUpdateStatus.ERROR)).hasSize(error);
|
||||
assertThat(targetManagement.findByUpdateStatus(PageRequest.of(0, 10), TargetUpdateStatus.IN_SYNC))
|
||||
.hasSize(inSync);
|
||||
}
|
||||
|
||||
private void assertActionStatusCount(final int total, final int running, final int warning, final int finished,
|
||||
final int canceled) {
|
||||
assertThat(countActionStatusAll()).isEqualTo(total);
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(running,
|
||||
new ActionStatusCondition(Status.RUNNING));
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(warning,
|
||||
new ActionStatusCondition(Status.WARNING));
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(canceled,
|
||||
new ActionStatusCondition(Status.CANCELED));
|
||||
assertThat(findActionStatusAll(PAGE).getContent()).haveAtLeast(finished,
|
||||
new ActionStatusCondition(Status.FINISHED));
|
||||
}
|
||||
|
||||
private void assertStatusAndActiveActionsCount(final TargetUpdateStatus status, final int activeActions) {
|
||||
final Target target = targetManagement.getByControllerID(DdiDeploymentBaseTest.DEFAULT_CONTROLLER_ID).get();
|
||||
assertThat(target.getUpdateStatus()).isEqualTo(status);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId()))
|
||||
.hasSize(activeActions);
|
||||
}
|
||||
|
||||
private Page<ActionStatus> findActionStatusAll(final Pageable pageable) {
|
||||
return JpaManagementHelper.findAllWithCountBySpec(actionStatusRepository, pageable, null);
|
||||
}
|
||||
|
||||
private static class ActionStatusCondition extends Condition<ActionStatus> {
|
||||
|
||||
private final Action.Status status;
|
||||
|
||||
public ActionStatusCondition(final Action.Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(final ActionStatus value) {
|
||||
return value.getStatus() == status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Bosch.IO 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.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
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.entity.ActionCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Artifact;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
/**
|
||||
* Test installed base from the controller.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Installed Base Resource")
|
||||
public class DdiInstalledBaseTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
ActionStatusRepository actionStatusRepository;
|
||||
private static final int ARTIFACT_SIZE = 5 * 1024;
|
||||
private static final String CONTROLLER_ID = "4715";
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the installed base resource is available as CBOR")
|
||||
public void installedBaseResourceCbor() throws Exception {
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = ds.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, target));
|
||||
postDeploymentFeedback(target.getControllerId(), actionId, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
// get installed base
|
||||
performGet(INSTALLED_BASE, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR), status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), actionId.toString());
|
||||
|
||||
// get artifacts
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.parseMediaType(DdiRestConstants.MEDIA_TYPE_CBOR),
|
||||
status().isOk(), tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
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
|
||||
putInstalledBase(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
|
||||
putInstalledBase(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
|
||||
putInstalledBase(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 {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final Artifact artifact1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
final Artifact artifact2 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds2), "test2", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature2 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds2), "test2.signature", ARTIFACT_SIZE);
|
||||
|
||||
// Run test with 1st action
|
||||
final Long actionId1 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(CONTROLLER_ID, actionId1.toString()))));
|
||||
|
||||
getAndVerifyDeploymentBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId1,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Closed")),
|
||||
status().isOk());
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
// Run test with 2nd action
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), target.getControllerId(), Action.ActionType.FORCED));
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId1.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(CONTROLLER_ID, actionId2.toString()))));
|
||||
|
||||
getAndVerifyDeploymentBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds2, artifact2, artifactSignature2,
|
||||
actionId2, ds2.findFirstModuleByType(osType).get().getId(), Action.ActionType.FORCED);
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId2,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Closed")),
|
||||
status().isOk());
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds2, artifact2, artifactSignature2,
|
||||
actionId2, ds2.findFirstModuleByType(osType).get().getId(), Action.ActionType.FORCED);
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId2.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
// older installed action is still accessible, although not part of controller
|
||||
// base
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test several deployments of same ds to a controller. Checks that cancelled action in history is not linked as installedBase.")
|
||||
public void deploymentActionsOfSameDsWithCancelledActionInHistory() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final Artifact artifact1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
// assign ds1, action1 - and provide cancel feedback
|
||||
final Long actionId1 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
deploymentManagement.cancelAction(actionId1);
|
||||
postCancelFeedback(target.getControllerId(), actionId1,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// assign ds1, action2 - and provide cancel feedback
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.FORCED));
|
||||
deploymentManagement.cancelAction(actionId2);
|
||||
postCancelFeedback(target.getControllerId(), actionId2,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// assign ds1, action 3 - and provide success feedback
|
||||
final Long actionId3 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
postDeploymentFeedback(target.getControllerId(), actionId3,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// Test: latest succeeded action is returned in installedBase
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId3.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId3, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
// cancelled action are not accessible
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId1.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId2.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test several deployments of same ds to a controller. Checks that latest cancelled action does not override actual installed ds.")
|
||||
public void deploymentActionsOfSameDsWithCancelledAction() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final Artifact artifact1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
|
||||
// assign ds1, action1 - and provide success feedback
|
||||
final Long actionId1 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
postDeploymentFeedback(target.getControllerId(), actionId1,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Success")),
|
||||
status().isOk());
|
||||
|
||||
// assign ds2, action2 - assign ds1, action 3 - and cancel both
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), target.getControllerId(), Action.ActionType.FORCED));
|
||||
final Long actionId3 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
deploymentManagement.cancelAction(actionId2);
|
||||
postCancelFeedback(target.getControllerId(), actionId2,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
deploymentManagement.cancelAction(actionId3);
|
||||
postCancelFeedback(target.getControllerId(), actionId3,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// Test: the succeeded action is returned in installedBase instead of the latest
|
||||
// cancelled action
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId1.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist());
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
// cancelled action are not accessible
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId2.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId3.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test several deployments of same ds to a controller. Checks that latest running action does not override actual installed ds.")
|
||||
public void deploymentActionsOfSameDsWithRunningAction() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1", true);
|
||||
final Artifact artifact1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature1 = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds1), "test1.signature", ARTIFACT_SIZE);
|
||||
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true);
|
||||
|
||||
// assign ds1, action1 - and provide success feedback
|
||||
final Long actionId1 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
postDeploymentFeedback(target.getControllerId(), actionId1,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Success")),
|
||||
status().isOk());
|
||||
|
||||
// assign ds2, action2 - assign ds1, action 3 - and cancel action 2
|
||||
final Long actionId2 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), target.getControllerId(), Action.ActionType.FORCED));
|
||||
final Long actionId3 = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds1.getId(), target.getControllerId(), Action.ActionType.SOFT));
|
||||
deploymentManagement.cancelAction(actionId2);
|
||||
postCancelFeedback(target.getControllerId(), actionId2,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Canceled")),
|
||||
status().isOk());
|
||||
|
||||
// Test: the succeeded action is returned in installedBase instead of the latest
|
||||
// cancelled action
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink(CONTROLLER_ID, actionId1.toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(CONTROLLER_ID, actionId3.toString()))));
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds1, artifact1, artifactSignature1,
|
||||
actionId1, ds1.findFirstModuleByType(osType).get().getId(), Action.ActionType.SOFT);
|
||||
|
||||
// cancelled action are not accessible
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId2.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId3.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test open deployment to a controller. Checks that installedBase returns 404 for a pending action.")
|
||||
public void installedBaseReturns404ForPendingAction() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, target));
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaTypes.HAL_JSON, status().isOk(), tenantAware.getCurrentTenant(), CONTROLLER_ID)
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink(CONTROLLER_ID, actionId.toString()))));
|
||||
|
||||
performGet(DEPLOYMENT_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId(), actionId.toString());
|
||||
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), target.getControllerId(),
|
||||
actionId.toString())).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that artifacts are found, after the action was already closed.")
|
||||
public void artifactsOfInstalledActionExist() throws Exception {
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long softwareModuleId = ds.getModules().stream().findAny().get().getId();
|
||||
testdataFactory.createArtifacts(softwareModuleId);
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(ds, target));
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId, getJsonClosedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
performGet(SOFTWARE_MODULE_ARTIFACTS, MediaType.APPLICATION_JSON, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), softwareModuleId.toString())
|
||||
.andExpect(jsonPath("$", hasSize(3)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename0')]", hasSize(1)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename1')]", hasSize(1)))
|
||||
.andExpect(jsonPath("$.[?(@.filename=='filename2')]", hasSize(1)));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("org.eclipse.hawkbit.ddi.rest.resource.DdiInstalledBaseTest#actionTypeForDeployment")
|
||||
@Description("Test forced deployment to a controller. Checks that action is represented as installedBase after installation.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 5), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 1),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1) })
|
||||
public void deploymentActionInInstalledBase(final Action.ActionType actionType) throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = createTargetAndAssertNoActiveActions();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("", true);
|
||||
final Artifact artifact = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE), getOsModule(ds),
|
||||
"test1", ARTIFACT_SIZE);
|
||||
final Artifact artifactSignature = testdataFactory.createArtifact(RandomUtils.nextBytes(ARTIFACT_SIZE),
|
||||
getOsModule(ds), "test1.signature", ARTIFACT_SIZE);
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), target.getControllerId(), actionType));
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Closed")),
|
||||
status().isOk());
|
||||
|
||||
// Run test
|
||||
final ResultActions resultActions = performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(),
|
||||
tenantAware.getCurrentTenant(), target.getControllerId());
|
||||
resultActions.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
containsString(String.format("/%s/controller/v1/%s/installedBase/%d",
|
||||
tenantAware.getCurrentTenant(), target.getControllerId(), actionId))));
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaType.APPLICATION_JSON, ds, artifact, artifactSignature,
|
||||
actionId, ds.findFirstModuleByType(osType).get().getId(), actionType);
|
||||
|
||||
getAndVerifyInstalledBasePayload(CONTROLLER_ID, MediaTypes.HAL_JSON, ds, artifact, artifactSignature, actionId,
|
||||
ds.findFirstModuleByType(osType).get().getId(), actionType);
|
||||
|
||||
// Action is still finished after calling installedBase
|
||||
final Iterable<ActionStatus> actionStatusMessages = deploymentManagement
|
||||
.findActionStatusByAction(PageRequest.of(0, 100, Sort.Direction.DESC, "id"), actionId);
|
||||
assertThat(actionStatusMessages).hasSize(2);
|
||||
final ActionStatus actionStatusMessage = actionStatusMessages.iterator().next();
|
||||
assertThat(actionStatusMessage.getStatus()).isEqualTo(Action.Status.FINISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test download-only deployment to a controller. Checks that download-only is not represented as installedBase.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 2) })
|
||||
public void deploymentDownloadOnlyActionNotInInstalledBase() throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), target.getControllerId(), Action.ActionType.DOWNLOAD_ONLY));
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").exists())
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist());
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId, getJsonDownloadDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
postDeploymentFeedback(target.getControllerId(), actionId, getJsonDownloadedDeploymentActionFeedback(),
|
||||
status().isOk());
|
||||
|
||||
// Test
|
||||
performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("org.eclipse.hawkbit.ddi.rest.resource.DdiInstalledBaseTest#actionTypeForDeployment")
|
||||
@Description("Test a failed deployment to a controller. Checks that closed action is not represented as installedBase.")
|
||||
public void deploymentActionFailedNotInInstalledBase(final Action.ActionType actionType) throws Exception {
|
||||
// Prepare test data
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
final Long actionId = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds.getId(), target.getControllerId(), actionType));
|
||||
|
||||
performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").exists())
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist());
|
||||
|
||||
postDeploymentFeedback(target.getControllerId(), actionId,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE),
|
||||
status().isOk());
|
||||
postDeploymentFeedback(target.getControllerId(), actionId,
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.FAILURE,
|
||||
Collections.singletonList("Installation failed")),
|
||||
status().isOk());
|
||||
|
||||
// Test
|
||||
performGet(CONTROLLER_BASE, MediaType.APPLICATION_JSON, status().isOk(), tenantAware.getCurrentTenant(),
|
||||
target.getControllerId()).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerInstalledAction endpoint.")
|
||||
public void testActionHistoryCount() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
postDeploymentFeedback(savedTarget.getControllerId(), savedAction.getId(),
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.SCHEDULED, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("Installation scheduled")),
|
||||
status().isOk());
|
||||
|
||||
postDeploymentFeedback(savedTarget.getControllerId(), savedAction.getId(),
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.PROCEEDING, DdiResult.FinalResult.NONE,
|
||||
Collections.singletonList("Installation proceeding")),
|
||||
status().isOk());
|
||||
// only this feedback triggers the ActionUpdateEvent
|
||||
postDeploymentFeedback(savedTarget.getControllerId(), savedAction.getId(),
|
||||
getJsonActionFeedback(DdiStatus.ExecutionStatus.CLOSED, DdiResult.FinalResult.SUCCESS,
|
||||
Collections.singletonList("Installation completed")),
|
||||
status().isOk());
|
||||
|
||||
// Test
|
||||
// for zero input no action history is returned
|
||||
mvc.perform(get(INSTALLED_BASE + "?actionHistory", tenantAware.getCurrentTenant(), 911, savedAction.getId())
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages").doesNotExist());
|
||||
|
||||
// depending on given query parameter value, only the latest messages are
|
||||
// returned
|
||||
mvc.perform(get(INSTALLED_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("Installation completed"))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation proceeding"))))
|
||||
.andExpect(
|
||||
jsonPath("$.actionHistory.messages", not(hasItem(containsString("Installation scheduled")))));
|
||||
|
||||
// for negative input the entire action history is returned
|
||||
mvc.perform(get(INSTALLED_BASE + "?actionHistory=-3", 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("Installation completed"))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation proceeding"))))
|
||||
.andExpect(jsonPath("$.actionHistory.messages", hasItem(containsString("Installation scheduled"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test various invalid access attempts to the installed resource und the expected behaviour of the server.")
|
||||
public void badInstalledAction() throws Exception {
|
||||
final Target target = testdataFactory.createTarget(CONTROLLER_ID);
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isMethodNotAllowed());
|
||||
|
||||
// non existing target
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), "not-existing", "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// no deployment
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, "1"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// wrong media type
|
||||
final List<Target> toAssign = Collections.singletonList(target);
|
||||
final DistributionSet savedSet = testdataFactory.createDistributionSet("");
|
||||
|
||||
final Long actionId = getFirstAssignedActionId(assignDistributionSet(savedSet, toAssign));
|
||||
postDeploymentFeedback(CONTROLLER_ID, actionId, getJsonClosedCancelActionFeedback(), status().isOk());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, actionId))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
mvc.perform(MockMvcRequestBuilders.get(INSTALLED_BASE, tenantAware.getCurrentTenant(), CONTROLLER_ID, actionId)
|
||||
.accept(MediaType.APPLICATION_ATOM_XML)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotAcceptable());
|
||||
}
|
||||
|
||||
private static Stream<Action.ActionType> actionTypeForDeployment() {
|
||||
return Stream.of(Action.ActionType.SOFT, Action.ActionType.FORCED);
|
||||
}
|
||||
|
||||
private Target createTargetAndAssertNoActiveActions() {
|
||||
final Target savedTarget = testdataFactory.createTarget(CONTROLLER_ID);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())).isEmpty();
|
||||
assertThat(deploymentManagement.countActionsAll()).isZero();
|
||||
assertThat(actionStatusRepository.count()).isZero();
|
||||
return savedTarget;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,731 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.SYSTEM_ROLE;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.TENANT_CONFIGURATION;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Step;
|
||||
import io.qameta.allure.Story;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiResult;
|
||||
import org.eclipse.hawkbit.ddi.json.model.DdiStatus;
|
||||
import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
|
||||
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.entity.ActionCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.ActionUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetTypeCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleTypeCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.TenantConfigurationCreatedEvent;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.Expect;
|
||||
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
|
||||
import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch;
|
||||
import org.eclipse.hawkbit.repository.test.util.WithUser;
|
||||
import org.eclipse.hawkbit.rest.util.JsonBuilder;
|
||||
import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
|
||||
import org.eclipse.hawkbit.util.IpUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
/**
|
||||
* Test the root controller resources.
|
||||
*/
|
||||
@Feature("Component Tests - Direct Device Integration API")
|
||||
@Story("Root Poll Resource")
|
||||
class DdiRootControllerTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static final String TARGET_COMPLETED_INSTALLATION_MSG = "Target completed installation.";
|
||||
private static final String TARGET_PROCEEDING_INSTALLATION_MSG = "Target proceeding installation.";
|
||||
private static final String TARGET_SCHEDULED_INSTALLATION_MSG = "Target scheduled installation.";
|
||||
@Autowired
|
||||
private HawkbitSecurityProperties securityProperties;
|
||||
|
||||
@Test
|
||||
@Description("Ensure that the root poll resource is available as CBOR")
|
||||
void rootPollResourceCbor() throws Exception {
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711).accept(DdiRestConstants.MEDIA_TYPE_CBOR))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the API returns JSON when no Accept header is specified by the client.")
|
||||
void apiReturnsJSONByDefault() throws Exception {
|
||||
final MvcResult result = mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711))
|
||||
.andDo(MockMvcResultPrinter.print()).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();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that targets cannot be created e.g. in plug'n play scenarios when tenant does not exists but can be created if the tenant exists.")
|
||||
@WithUser(tenantId = "tenantDoesNotExists", allSpPermissions = true, authorities = { CONTROLLER_ROLE,
|
||||
SYSTEM_ROLE }, autoCreateTenant = false)
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetTypeCreatedEvent.class, count = 3),
|
||||
@Expect(type = SoftwareModuleTypeCreatedEvent.class, count = 2) })
|
||||
void targetCannotBeRegisteredIfTenantDoesNotExistsButWhenExists() throws Exception {
|
||||
|
||||
mvc.perform(get("/default-tenant/", tenantAware.getCurrentTenant())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
// create tenant -- creates softwaremoduletypes and distributionsettypes
|
||||
systemManagement.createTenantMetadata("tenantDoesNotExists");
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), "aControllerId"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
|
||||
// delete tenant again, will also deleted target aControllerId
|
||||
systemManagement.deleteTenant("tenantDoesNotExists");
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), "aControllerId"))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that target poll request does not change audit data on the entity.")
|
||||
@WithUser(principal = "knownPrincipal", authorities = { SpPermission.READ_TARGET, SpPermission.UPDATE_TARGET,
|
||||
SpPermission.CREATE_TARGET }, allSpPermissions = false)
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) })
|
||||
void targetPollDoesNotModifyAuditData() throws Exception {
|
||||
// create target first with "knownPrincipal" user and audit data
|
||||
final String knownTargetControllerId = "target1";
|
||||
final String knownCreatedBy = "knownPrincipal";
|
||||
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),
|
||||
() -> {
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownTargetControllerId))
|
||||
.andDo(MockMvcResultPrinter.print()).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());
|
||||
assertThat(targetVerify.getCreatedAt()).isEqualTo(findTargetByControllerID.getCreatedAt());
|
||||
assertThat(targetVerify.getLastModifiedBy()).isEqualTo(findTargetByControllerID.getLastModifiedBy());
|
||||
assertThat(targetVerify.getLastModifiedAt()).isEqualTo(findTargetByControllerID.getLastModifiedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that server returns a not found response in case of empty controller ID.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 0) })
|
||||
void rootRsWithoutId() throws Exception {
|
||||
mvc.perform(get("/controller/v1/")).andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the system creates a new target in plug and play manner, i.e. target is authenticated but does not exist yet.")
|
||||
@ExpectEvents({ @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";
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", 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())
|
||||
.isGreaterThanOrEqualTo(current);
|
||||
|
||||
assertThat(targetManagement.getByControllerID(controllerId).get().getUpdateStatus())
|
||||
.isEqualTo(TargetUpdateStatus.REGISTERED);
|
||||
|
||||
// not allowed methods
|
||||
mvc.perform(post(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(put(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
|
||||
mvc.perform(delete(CONTROLLER_BASE, "default-tenant", controllerId)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isMethodNotAllowed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that tenant specific polling time, which is saved in the db, is delivered to the controller.")
|
||||
@WithUser(principal = "knownpricipal", allSpPermissions = false)
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1),
|
||||
@Expect(type = TenantConfigurationCreatedEvent.class, count = 1) })
|
||||
void pollWithModifiedGlobalPollingTime() throws Exception {
|
||||
SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION),
|
||||
() -> {
|
||||
tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME_INTERVAL,
|
||||
"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;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that etag check results in not modified response if provided etag by client is identical to entity in repository.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 6),
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 3), @Expect(type = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 2),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 6),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 2), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 6), // implicit lock
|
||||
@Expect(type = ActionCreatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void rootRsNotModified() throws Exception {
|
||||
final String controllerId = "4711";
|
||||
final String etag = mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaTypes.HAL_JSON))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.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(PAGE, target.getControllerId())
|
||||
.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()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href").doesNotExist())
|
||||
.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()).andExpect(status().isNotModified());
|
||||
|
||||
// now lets finish the update
|
||||
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
|
||||
.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId)
|
||||
.header("If-None-Match", etag).accept(MediaType.APPLICATION_JSON)
|
||||
.with(new RequestOnHawkbitDefaultPortPostProcessor()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href").doesNotExist())
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink("4711", updateAction.getId().toString()))))
|
||||
.andReturn().getResponse().getHeader("ETag");
|
||||
|
||||
// Now another deployment
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2");
|
||||
|
||||
assignDistributionSet(ds2.getId(), controllerId);
|
||||
|
||||
final Action updateAction2 = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), controllerId)
|
||||
.header("If-None-Match", etagAfterInstallation).accept(MediaType.APPLICATION_JSON)
|
||||
.with(new RequestOnHawkbitDefaultPortPostProcessor()))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", equalTo("00:01:00")))
|
||||
.andExpect(jsonPath("$._links.installedBase.href",
|
||||
startsWith(installedBaseLink("4711", updateAction.getId().toString()))))
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href",
|
||||
startsWith(deploymentBaseLink("4711", updateAction2.getId().toString()))))
|
||||
.andReturn().getResponse().getHeader("ETag");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the target state machine of a precomissioned target switches from "
|
||||
+ "UNKNOWN to REGISTERED when the target polls for the first time.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.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);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the source IP address of the polling target is correctly stored in repository")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = TargetPollEvent.class, count = 1) })
|
||||
void rootRsPlugAndPlayIpAddress() throws Exception {
|
||||
// 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),
|
||||
() -> {
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownControllerId1))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
|
||||
return null;
|
||||
});
|
||||
|
||||
// verify
|
||||
final Target target = targetManagement.getByControllerID(knownControllerId1).get();
|
||||
assertThat(target.getAddress()).isEqualTo(IpUtil.createHttpUri("127.0.0.1"));
|
||||
assertThat(target.getCreatedBy()).isEqualTo("CONTROLLER_PLUG_AND_PLAY");
|
||||
assertThat(target.getCreatedAt()).isGreaterThanOrEqualTo(create);
|
||||
assertThat(target.getLastModifiedBy()).isEqualTo("CONTROLLER_PLUG_AND_PLAY");
|
||||
assertThat(target.getLastModifiedAt()).isGreaterThanOrEqualTo(create);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that the source IP address of the polling target is not stored in repository if disabled")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@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);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Controller trys to finish an update process after it has been finished by an error action status.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = ActionUpdatedEvent.class, count = 1),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2) })
|
||||
void tryToFinishAnUpdateProcessAfterItHasBeenFinished() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.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());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Controller sends attribute update request after device successfully closed software update.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@Expect(type = DistributionSetUpdatedEvent.class, count = 1), // implicit lock
|
||||
@Expect(type = SoftwareModuleUpdatedEvent.class, count = 3), // implicit lock
|
||||
@Expect(type = TargetAssignDistributionSetEvent.class, count = 2),
|
||||
@Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 6), @Expect(type = TargetPollEvent.class, count = 4),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void attributeUpdateRequestSendingAfterSuccessfulDeployment() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("1");
|
||||
final Target savedTarget = testdataFactory.createTarget("922");
|
||||
final Map<String, String> 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);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that only a specific count of messages are returned based on the input actionHistory for getControllerDeploymentActionFeedback endpoint.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void testActionHistoryCount() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.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)))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that a zero input value of actionHistory results in no action history appended for getControllerDeploymentActionFeedback endpoint.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void testActionHistoryZeroInput() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.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()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.actionHistory.messages").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test to verify that entire action history is returned if the input value for actionHistory is -1, for getControllerDeploymentActionFeedback endpoint.")
|
||||
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1),
|
||||
@Expect(type = DistributionSetCreatedEvent.class, count = 1),
|
||||
@Expect(type = SoftwareModuleCreatedEvent.class, count = 3),
|
||||
@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 = ActionUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetUpdatedEvent.class, count = 2),
|
||||
@Expect(type = TargetAttributesRequestedEvent.class, count = 1) })
|
||||
void testActionHistoryNegativeInput() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
Target savedTarget = testdataFactory.createTarget("911");
|
||||
savedTarget = getFirstAssignedTarget(assignDistributionSet(ds.getId(), savedTarget.getControllerId()));
|
||||
final Action savedAction = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.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))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test the polling time based on different maintenance window start and end time.")
|
||||
void sleepTimeResponseForDifferentMaintenanceWindowParameters() throws Exception {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("");
|
||||
|
||||
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");
|
||||
return null;
|
||||
});
|
||||
|
||||
final Target savedTarget = testdataFactory.createTarget("1911");
|
||||
assignDistributionSetWithMaintenanceWindow(ds.getId(), savedTarget.getControllerId(), getTestSchedule(16),
|
||||
getTestDuration(10), getTestTimeZone()).getAssignedEntity().iterator().next();
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.config.polling.sleep", greaterThanOrEqualTo("00:05:00")));
|
||||
|
||||
final Target savedTarget1 = testdataFactory.createTarget("2911");
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet("1");
|
||||
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")))
|
||||
.andExpect(jsonPath("$.config.polling.sleep", greaterThanOrEqualTo("00:03:00")));
|
||||
|
||||
final Target savedTarget2 = testdataFactory.createTarget("3911");
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet("2");
|
||||
assignDistributionSetWithMaintenanceWindow(ds2.getId(), savedTarget2.getControllerId(), getTestSchedule(5),
|
||||
getTestDuration(5), getTestTimeZone()).getAssignedEntity().iterator().next();
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", "3911")).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.config.polling.sleep", lessThan("00:02:00")));
|
||||
|
||||
final Target savedTarget3 = testdataFactory.createTarget("4911");
|
||||
final DistributionSet ds3 = testdataFactory.createDistributionSet("3");
|
||||
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")));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test download and update values before maintenance window start time.")
|
||||
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());
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
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")))
|
||||
.andExpect(jsonPath("$.deployment.update", equalTo("skip")))
|
||||
.andExpect(jsonPath("$.deployment.maintenanceWindow", equalTo("unavailable")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Test download and update values after maintenance window start time.")
|
||||
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()));
|
||||
|
||||
mvc.perform(get(CONTROLLER_BASE, "default-tenant", "1911")).andExpect(status().isOk());
|
||||
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, savedTarget.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
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")))
|
||||
.andExpect(jsonPath("$.deployment.update", equalTo("forced")))
|
||||
.andExpect(jsonPath("$.deployment.maintenanceWindow", equalTo("available")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Assign multiple DS in multi-assignment mode. The earliest active Action is exposed to the controller.")
|
||||
void earliestActionIsExposedToControllerInMultiAssignMode() throws Exception {
|
||||
enableMultiAssignments();
|
||||
final Target target = testdataFactory.createTarget();
|
||||
final DistributionSet ds1 = testdataFactory.createDistributionSet(UUID.randomUUID().toString());
|
||||
final DistributionSet ds2 = testdataFactory.createDistributionSet(UUID.randomUUID().toString());
|
||||
final Action action1 = getFirstAssignedAction(assignDistributionSet(ds1.getId(), target.getControllerId(), 56));
|
||||
final Long action2Id = getFirstAssignedActionId(
|
||||
assignDistributionSet(ds2.getId(), target.getControllerId(), 34));
|
||||
|
||||
assertDeploymentActionIsExposedToTarget(target.getControllerId(), action1.getId());
|
||||
sendDeploymentActionFeedback(target, action1, "closed", "success");
|
||||
assertDeploymentActionIsExposedToTarget(target.getControllerId(), action2Id);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("The system should not create a new target because of a too long controller id.")
|
||||
void rootRsWithInvalidControllerId() throws Exception {
|
||||
final String invalidControllerId = RandomStringUtils.randomAlphabetic(Target.CONTROLLER_ID_MAX_SIZE + 1);
|
||||
mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), invalidControllerId))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Step
|
||||
private void assertAttributesUpdateNotRequestedAfterFailedDeployment(Target target, final DistributionSet ds)
|
||||
throws Exception {
|
||||
target = getFirstAssignedTarget(assignDistributionSet(ds.getId(), target.getControllerId()));
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
sendDeploymentActionFeedback(target, action, "closed", "failure").andExpect(status().isOk());
|
||||
assertThatAttributesUpdateIsNotRequested(target.getControllerId());
|
||||
}
|
||||
|
||||
@Step
|
||||
private void assertAttributesUpdateRequestedAfterSuccessfulDeployment(Target target, final DistributionSet ds)
|
||||
throws Exception {
|
||||
target = getFirstAssignedTarget(assignDistributionSet(ds.getId(), target.getControllerId()));
|
||||
final Action action = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
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)).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)).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 {
|
||||
if (finished == null) {
|
||||
finished = "none";
|
||||
}
|
||||
if (message == null) {
|
||||
message = RandomStringUtils.randomAlphanumeric(1000);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
private ResultActions sendDeploymentActionFeedback(final Target target, final Action action, final String execution,
|
||||
final String finished) throws Exception {
|
||||
return sendDeploymentActionFeedback(target, action, execution, finished, null);
|
||||
}
|
||||
|
||||
private void assertDeploymentActionIsExposedToTarget(final String controllerId, final long expectedActionId)
|
||||
throws Exception {
|
||||
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))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.deploymentBase.href", containsString(expectedDeploymentBaseLink)));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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.rest.resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.security.DosFilter;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
/**
|
||||
* Test potential DOS attack scenarios and check if the filter prevents them.
|
||||
*/
|
||||
@ActiveProfiles({ "test" })
|
||||
@Feature("Component Tests - REST Security")
|
||||
@Story("Denial of Service protection filter")
|
||||
class DosFilterTest extends AbstractDDiApiIntegrationTest {
|
||||
|
||||
private static final String X_FORWARDED_FOR = HawkbitSecurityProperties.Clients.X_FORWARDED_FOR;
|
||||
|
||||
@Override
|
||||
protected DefaultMockMvcBuilder createMvcWebAppContext(final WebApplicationContext context) {
|
||||
return super.createMvcWebAppContext(context).addFilter(
|
||||
new DosFilter(null, 10, 10,
|
||||
"127\\.0\\.0\\.1|\\[0:0:0:0:0:0:0:1\\]", "(^192\\.168\\.)",
|
||||
"X-Forwarded-For"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that clients that are on the blacklist are forbidden")
|
||||
void blackListedClientIsForbidden() throws Exception {
|
||||
mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "192.168.0.4 , 10.0.0.1 ")).andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a READ DoS attempt is blocked ")
|
||||
void getFloodingAttackThatIsPrevented() throws Exception {
|
||||
|
||||
MvcResult result = null;
|
||||
|
||||
int requests = 0;
|
||||
do {
|
||||
result = mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "10.0.0.1")).andReturn();
|
||||
requests++;
|
||||
|
||||
// we give up after 1.000 requests
|
||||
assertThat(requests).isLessThan(1_000);
|
||||
} while (result.getResponse().getStatus() != HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
|
||||
// the filter shuts down after 100 GET requests
|
||||
assertThat(requests).isGreaterThanOrEqualTo(10);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that an assumed READ DoS attempt is not blocked as the client (with IPv4 address) is on a whitelist")
|
||||
void unacceptableGetLoadButOnWhitelistIPv4() throws Exception {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "127.0.0.1")).andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that an assumed READ DoS attempt is not blocked as the client (with IPv6 address) is on a whitelist")
|
||||
void unacceptableGetLoadButOnWhitelistIPv6() throws Exception {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "0:0:0:0:0:0:0:1")).andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a relatively high number of READ requests is allowed if it is below the DoS detection threshold")
|
||||
@SuppressWarnings("squid:S2925")
|
||||
// No idea how to get rid of the Thread.sleep here
|
||||
void acceptableGetLoad() throws Exception {
|
||||
|
||||
for (int x = 0; x < 3; x++) {
|
||||
// sleep for one second
|
||||
Thread.sleep(1100);
|
||||
for (int i = 0; i < 9; i++) {
|
||||
mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
|
||||
.header(X_FORWARDED_FOR, "10.0.0.1")).andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a WRITE DoS attempt is blocked ")
|
||||
void putPostFloddingAttackThatisPrevented() throws Exception {
|
||||
final Long actionId = prepareDeploymentBase();
|
||||
final String feedback = getJsonProceedingDeploymentActionFeedback();
|
||||
|
||||
MvcResult result = null;
|
||||
int requests = 0;
|
||||
do {
|
||||
result = mvc.perform(post("/{tenant}/controller/v1/4711/deploymentBase/" + actionId + "/feedback",
|
||||
tenantAware.getCurrentTenant()).header(X_FORWARDED_FOR, "10.0.0.1").content(feedback)
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
|
||||
.andReturn();
|
||||
requests++;
|
||||
|
||||
// we give up after 500 requests
|
||||
assertThat(requests).isLessThan(500);
|
||||
} while (result.getResponse().getStatus() != HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
|
||||
// the filter shuts down after 10 POST requests
|
||||
assertThat(requests).isGreaterThanOrEqualTo(10);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Ensures that a relatively high number of WRITE requests is allowed if it is below the DoS detection threshold")
|
||||
@SuppressWarnings("squid:S2925")
|
||||
// No idea how to get rid of the Thread.sleep here
|
||||
void acceptablePutPostLoad() throws Exception {
|
||||
final Long actionId = prepareDeploymentBase();
|
||||
final String feedback = getJsonProceedingDeploymentActionFeedback();
|
||||
|
||||
for (int x = 0; x < 5; x++) {
|
||||
// sleep for one second
|
||||
Thread.sleep(1100);
|
||||
|
||||
for (int i = 0; i < 9; i++) {
|
||||
mvc.perform(post("/{tenant}/controller/v1/4711/deploymentBase/" + actionId + "/feedback",
|
||||
tenantAware.getCurrentTenant()).header(X_FORWARDED_FOR, "10.0.0.1")
|
||||
.content(feedback).contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Long prepareDeploymentBase() {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("test");
|
||||
final Target target = testdataFactory.createTarget("4711");
|
||||
final List<Target> toAssign = Collections.singletonList(target);
|
||||
|
||||
assignDistributionSet(ds, toAssign);
|
||||
assertThat(deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())).hasSize(1);
|
||||
|
||||
final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
|
||||
.getContent().get(0);
|
||||
|
||||
return uaction.getId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ddi.rest.resource;
|
||||
|
||||
import static org.eclipse.hawkbit.ddi.rest.resource.AbstractDDiApiIntegrationTest.HTTP_PORT;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
|
||||
public class RequestOnHawkbitDefaultPortPostProcessor implements RequestPostProcessor {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
|
||||
request.setRemotePort(HTTP_PORT);
|
||||
request.setServerPort(HTTP_PORT);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
# DDI configuration - START
|
||||
hawkbit.controller.pollingTime=00:01:00
|
||||
hawkbit.controller.pollingOverdueTime=00:01:00
|
||||
hawkbit.controller.minPollingTime=00:00:30
|
||||
hawkbit.controller.maintenanceWindowPollCount=3
|
||||
# DDI configuration - END
|
||||
# Upload configuration - START
|
||||
spring.servlet.multipart.max-file-size=5MB
|
||||
# Upload configuration - END
|
||||
# Quota - START
|
||||
hawkbit.server.security.dos.maxStatusEntriesPerAction=100
|
||||
hawkbit.server.security.dos.maxAttributeEntriesPerTarget=10
|
||||
# Quota - END
|
||||
# Logging START - activate to see request/response details
|
||||
#logging.level.org.eclipse.hawkbit.rest.util.MockMvcResultPrinter=DEBUG
|
||||
# Logging END
|
||||
60
hawkbit-ddi/hawkbit-ddi-server/README.md
Normal file
60
hawkbit-ddi/hawkbit-ddi-server/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# hawkBit DDI Server (EXPERIMENTAL!)
|
||||
|
||||
The hawkBit DDI Server is a standalone spring-boot application with an embedded servlet container. It should be started
|
||||
with at least hawkbit-mgmt-server.
|
||||
|
||||
## On your own workstation
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
java -jar hawkbit-runtime/hawkbit-ddi-server/target/hawkbit-ddi-server-*-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
_(Note: you have to add the JDBC driver also to your class path if you intend to use another database than H2.)_
|
||||
|
||||
Or:
|
||||
|
||||
```bash
|
||||
run org.eclipse.hawkbit.app.ddi.DDIStart
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
The Management API can be accessed via http://localhost:8081/rest/v1
|
||||
The root url http://localhost:8081 will redirect directly to the Swagger Management UI
|
||||
|
||||
### Clustering (Experimental!!!)
|
||||
|
||||
The micro-service instances are configured to communicate via Spring Cloud Bus. You could run multiple instances of any
|
||||
micro-service but hawkbit-mgmt-server. Management server run some schedulers which shall not run simultaneously - e.g.
|
||||
auto assignment checker and rollouts executor. To run multiple management server instances you shall do some extensions
|
||||
of hawkbit to ensure that they wont run schedulers simultaneously or you shall configure all instances but one to do not
|
||||
run schedulers!
|
||||
|
||||
## Optional Protostuff for Sprign cloud bus
|
||||
|
||||
The micro-service instances are configured to communicate via Spring Cloud Bus. Optionally, you could
|
||||
use [Protostuff](https://github.com/protostuff/protostuff) based message payload serialization for improved performance.
|
||||
|
||||
**Note**: If Protostuff is enabled it shall be enabled on all microservices!
|
||||
|
||||
Add/Uncomment to/in your `application.properties` :
|
||||
|
||||
```properties
|
||||
spring.cloud.stream.bindings.springCloudBusInput.content-type=application/binary+protostuff
|
||||
spring.cloud.stream.bindings.springCloudBusOutput.content-type=application/binary+protostuff
|
||||
```
|
||||
|
||||
Add to your `pom.xml` :
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.protostuff</groupId>
|
||||
<artifactId>protostuff-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.protostuff</groupId>
|
||||
<artifactId>protostuff-runtime</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
94
hawkbit-ddi/hawkbit-ddi-server/pom.xml
Normal file
94
hawkbit-ddi/hawkbit-ddi-server/pom.xml
Normal file
@@ -0,0 +1,94 @@
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-ddi-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-ddi-server</artifactId>
|
||||
<name>hawkBit :: Runtime :: DDI Server</name>
|
||||
|
||||
<properties>
|
||||
<spring.app.class>org.eclipse.hawkbit.app.ddi.DDIStart</spring.app.class>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-boot-starter-ddi-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-repository-test</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- if run against mysql -->
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${baseDir}</outputDirectory>
|
||||
<mainClass>${spring.app.class}</mainClass>
|
||||
<layout>JAR</layout>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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.app.ddi;
|
||||
|
||||
import org.eclipse.hawkbit.autoconfigure.security.EnableHawkbitManagedSecurityConfiguration;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
/**
|
||||
* A {@link SpringBootApplication} annotated class with a main method to start.
|
||||
* The minimal configuration for the stand alone hawkBit DDI server.
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = "org.eclipse.hawkbit")
|
||||
@EnableHawkbitManagedSecurityConfiguration
|
||||
public class DDIStart {
|
||||
|
||||
/**
|
||||
* Main method to start the spring-boot application.
|
||||
*
|
||||
* @param args the VM arguments.
|
||||
*/
|
||||
public static void main(final String[] args) {
|
||||
SpringApplication.run(DDIStart.class, args);
|
||||
}
|
||||
|
||||
@Controller
|
||||
public static class RedirectController {
|
||||
|
||||
@GetMapping("/")
|
||||
public RedirectView redirectToSwagger(
|
||||
RedirectAttributes attributes) {
|
||||
attributes.addFlashAttribute("flashAttribute", "redirectWithRedirectView");
|
||||
attributes.addAttribute("attribute", "redirectWithRedirectView");
|
||||
return new RedirectView("swagger-ui/index.html");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, proxyTargetClass = true)
|
||||
public static class MethodSecurityConfig {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright (c) 2019 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
|
||||
#
|
||||
|
||||
# This profile adds basic configurations for a DB2 DB usage.
|
||||
# Keep in mind that you need the DB2 driver in your classpath on compile.
|
||||
# see https://www.eclipse.org/hawkbit/guides/runhawkbit/
|
||||
|
||||
spring.jpa.database=DB2
|
||||
spring.datasource.url=jdbc:db2://localhost:50000/hawkbit
|
||||
spring.datasource.username=db2inst1
|
||||
spring.datasource.password=db2inst1-pwd
|
||||
spring.datasource.driverClassName=com.ibm.db2.jcc.DB2Driver
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright (c) 2018 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
|
||||
#
|
||||
|
||||
# This profile adds basic configurations for a Microsoft SQL Server DB usage.
|
||||
# Keep in mind that you need the SQL server driver in your classpath on compile.
|
||||
# see https://www.eclipse.org/hawkbit/guides/runhawkbit/
|
||||
|
||||
spring.jpa.database=SQL_SERVER
|
||||
spring.datasource.url=jdbc:sqlserver://localhost:1433;database=hawkbit
|
||||
spring.datasource.username=SA
|
||||
spring.datasource.password=
|
||||
spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
# This profile adds basic configurations for a MySQL DB usage.
|
||||
# Keep in mind that you need the MariaDB driver in your classpath on compile.
|
||||
# see https://www.eclipse.org/hawkbit/guides/runhawkbit/
|
||||
|
||||
spring.jpa.database=MYSQL
|
||||
spring.datasource.url=jdbc:mariadb://localhost:3306/hawkbit
|
||||
spring.datasource.username=root
|
||||
spring.datasource.password=
|
||||
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright (c) 2020 Enapter Co.,Ltd
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
# This profile adds basic configurations for a PostgreSQL usage.
|
||||
# Keep in mind that you need the PostgreSQL driver in your classpath on compile.
|
||||
# see https://www.eclipse.org/hawkbit/guides/runhawkbit/
|
||||
|
||||
spring.jpa.database=POSTGRESQL
|
||||
spring.datasource.url=jdbc:postgresql://localhost:5432/hawkbit
|
||||
spring.datasource.username=postgres
|
||||
spring.datasource.password=admin
|
||||
spring.datasource.driverClassName=org.postgresql.Driver
|
||||
@@ -0,0 +1,83 @@
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
# Spring configuration
|
||||
spring.application.name=ddi-server
|
||||
spring.main.allow-bean-definition-overriding=true
|
||||
server.port=8081
|
||||
|
||||
# Logging configuration
|
||||
logging.level.org.eclipse.hawkbit.eventbus.DeadEventListener=WARN
|
||||
logging.level.org.springframework.boot.actuate.audit.listener.AuditListener=WARN
|
||||
logging.level.org.hibernate.validator.internal.util.Version=WARN
|
||||
# security Log with hints on potential attacks
|
||||
logging.level.server-security=INFO
|
||||
# logging pattern
|
||||
logging.pattern.console=%clr(%d{${logging.pattern.dateformat:yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${logging.pattern.level:%5p}) %clr(${PID:}){magenta} %clr(---){faint} %clr([${spring.application.name}] [%X{tenant}:%X{user}] [%15.15t]){faint} %clr(${logging.pattern.correlation:}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${logging.exception-conversion-word:%wEx}
|
||||
|
||||
# User Security
|
||||
spring.security.user.name=admin
|
||||
spring.security.user.password={noop}admin
|
||||
|
||||
# Http Encoding
|
||||
server.servlet.encoding.charset=UTF-8
|
||||
server.servlet.encoding.enabled=true
|
||||
server.servlet.encoding.force=true
|
||||
|
||||
# DDI authentication configuration
|
||||
hawkbit.server.ddi.security.authentication.anonymous.enabled=false
|
||||
hawkbit.server.ddi.security.authentication.targettoken.enabled=false
|
||||
hawkbit.server.ddi.security.authentication.gatewaytoken.enabled=false
|
||||
|
||||
# Optional events
|
||||
hawkbit.server.repository.publish-target-poll-event=false
|
||||
|
||||
## Configuration for DMF/RabbitMQ integration
|
||||
spring.rabbitmq.username=guest
|
||||
spring.rabbitmq.password=guest
|
||||
spring.rabbitmq.virtual-host=/
|
||||
spring.rabbitmq.host=localhost
|
||||
spring.rabbitmq.port=5672
|
||||
|
||||
# Enable CORS and specify the allowed origins:
|
||||
#hawkbit.server.security.cors.enabled=true
|
||||
#hawkbit.server.security.cors.allowedOrigins=http://localhost
|
||||
|
||||
# Swagger Configuration
|
||||
springdoc.api-docs.version=openapi_3_0
|
||||
springdoc.show-oauth2-endpoints=true
|
||||
springdoc.show-login-endpoint=true
|
||||
springdoc.packages-to-scan=org.eclipse.hawkbit.ddi
|
||||
springdoc.paths-to-exclude=/system/**
|
||||
|
||||
# Flyway disabled - US only
|
||||
spring.flyway.enabled=false
|
||||
## SQL Database Configuration - END
|
||||
|
||||
## No Schedulers - START
|
||||
hawkbit.autoassign.scheduler.enabled=false
|
||||
hawkbit.rollout.scheduler.enabled=false
|
||||
## No Schedulers - END
|
||||
|
||||
# Disable discovery client of spring-cloud-commons
|
||||
spring.cloud.discovery.enabled=false
|
||||
# Enable communication between services
|
||||
spring.cloud.bus.enabled=true
|
||||
spring.cloud.bus.ack.enabled=false
|
||||
spring.cloud.bus.refresh.enabled=false
|
||||
spring.cloud.bus.env.enabled=false
|
||||
endpoints.spring.cloud.bus.refresh.enabled=false
|
||||
endpoints.spring.cloud.bus.env.enabled=false
|
||||
spring.cloud.stream.bindings.springCloudBusInput.group=ddi-server
|
||||
|
||||
# To use protostuff (for instance fot improved performance) you shall uncomment
|
||||
# the following two lines and add io.protostuff:protostuff-core and io.protostuff:protostuff-runtime to dependencies
|
||||
#spring.cloud.stream.bindings.springCloudBusInput.content-type=application/binary+protostuff
|
||||
#spring.cloud.stream.bindings.springCloudBusOutput.content-type=application/binary+protostuff
|
||||
14
hawkbit-ddi/hawkbit-ddi-server/src/main/resources/banner.txt
Normal file
14
hawkbit-ddi/hawkbit-ddi-server/src/main/resources/banner.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
______ _ _ _ _ ____ _ _ _____ _____ _____
|
||||
| ____| | (_) | | | | | _ \(_) | | __ \| __ \_ _|
|
||||
| |__ ___| |_ _ __ ___ ___ | |__ __ ___ _| | _| |_) |_| |_ | | | | | | || |
|
||||
| __| / __| | | '_ \/ __|/ _ \ | '_ \ / _` \ \ /\ / / |/ / _ <| | __| | | | | | | || |
|
||||
| |___| (__| | | |_) \__ \ __/ | | | | (_| |\ V V /| <| |_) | | |_ | |__| | |__| || |_
|
||||
|______\___|_|_| .__/|___/\___| |_| |_|\__,_| \_/\_/ |_|\_\____/|_|\__| |_____/|_____/_____|
|
||||
| |
|
||||
|_|
|
||||
|
||||
Eclipse hawkBit DDI Server ${application.formatted-version}
|
||||
using Spring Boot ${spring-boot.formatted-version}
|
||||
|
||||
Go to https://www.eclipse.org/hawkbit for more information.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2020 Bosch.IO 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.app.ddi;
|
||||
|
||||
import org.eclipse.hawkbit.repository.test.util.SharedSqlTestDatabaseExtension;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
@SpringBootTest(properties = { "hawkbit.dmf.rabbitmq.enabled=false" })
|
||||
@ExtendWith(SharedSqlTestDatabaseExtension.class)
|
||||
public abstract class AbstractSecurityTest {
|
||||
|
||||
protected MockMvc mvc;
|
||||
@Autowired
|
||||
private WebApplicationContext context;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
final DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(context)
|
||||
.apply(SecurityMockMvcConfigurers.springSecurity()).dispatchOptions(true);
|
||||
mvc = builder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2019 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.app.ddi;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
@TestPropertySource(properties = { "hawkbit.server.security.allowedHostNames=localhost",
|
||||
"hawkbit.server.security.httpFirewallIgnoredPaths=/index.html" })
|
||||
@Feature("Integration Test - Security")
|
||||
@Story("Allowed Host Names")
|
||||
public class AllowedHostNamesTest extends AbstractSecurityTest {
|
||||
|
||||
@Test
|
||||
@Description("Tests whether a RequestRejectedException is thrown when not allowed host is used")
|
||||
public void allowedHostNameWithNotAllowedHost() throws Exception {
|
||||
mvc.perform(get("/").header(HttpHeaders.HOST, "www.google.com")).andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests whether request is redirected when allowed host is used")
|
||||
public void allowedHostNameWithAllowedHost() throws Exception {
|
||||
mvc.perform(get("/").header(HttpHeaders.HOST, "localhost")).andExpect(status().is3xxRedirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests whether request without allowed host name and with ignored path end up with a client error")
|
||||
public void notAllowedHostnameWithIgnoredPath() throws Exception {
|
||||
mvc.perform(get("/index.html").header(HttpHeaders.HOST, "www.google.com"))
|
||||
.andExpect(status().is4xxClientError());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Bosch.IO 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.app.ddi;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||
import org.eclipse.hawkbit.repository.test.util.WithUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
@Feature("Integration Test - Security")
|
||||
@Story("PreAuthorized enabled")
|
||||
@TestPropertySource(properties = { "spring.flyway.enabled=true" })
|
||||
public class PreAuthorizeEnabledTest extends AbstractSecurityTest {
|
||||
|
||||
@Test
|
||||
@Description("Tests whether request fail if a role is forbidden for the user")
|
||||
@WithUser(authorities = { SpPermission.READ_TARGET })
|
||||
public void failIfNoRole() throws Exception {
|
||||
mvc.perform(get("/DEFAULT/controller/v1/controllerId")).andExpect(result ->
|
||||
assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.FORBIDDEN.value()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Tests whether request succeed if a role is granted for the user")
|
||||
@WithUser(authorities = { SpPermission.SpringEvalExpressions.CONTROLLER_ROLE })
|
||||
public void successIfHasRole() throws Exception {
|
||||
mvc.perform(get("/DEFAULT/controller/v1/controllerId")).andExpect(result -> {
|
||||
assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value());
|
||||
});
|
||||
}
|
||||
}
|
||||
31
hawkbit-ddi/pom.xml
Normal file
31
hawkbit-ddi/pom.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-ddi-parent</artifactId>
|
||||
<name>hawkBit :: DDI</name>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>hawkbit-ddi-api</module>
|
||||
<module>hawkbit-ddi-resource</module>
|
||||
<module>hawkbit-boot-starter-ddi-api</module>
|
||||
<module>hawkbit-ddi-server</module>
|
||||
</modules>
|
||||
</project>
|
||||
Reference in New Issue
Block a user