diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/AbstractArtifactRepository.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/AbstractArtifactRepository.java index 7b182636b..97515c3c5 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/AbstractArtifactRepository.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/artifact/repository/AbstractArtifactRepository.java @@ -29,7 +29,6 @@ import com.google.common.io.ByteStreams; /** * Abstract utility class for ArtifactRepository implementations with common * functionality, e.g. computation of hashes. - * */ public abstract class AbstractArtifactRepository implements ArtifactRepository { @@ -71,7 +70,7 @@ public abstract class AbstractArtifactRepository implements ArtifactRepository { } catch (final IOException e) { throw new ArtifactStoreException(e.getMessage(), e); } finally { - if (file.exists() && !file.delete()) { + if (file != null && file.exists() && !file.delete()) { LOG.error("Could not delete temp file {}", file); } } diff --git a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java index 948991263..1703a21c3 100644 --- a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java +++ b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java @@ -60,8 +60,6 @@ import ru.yandex.qatools.allure.annotations.Stories; @SpringApplicationConfiguration(classes = DownloadTestConfiguration.class) public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { - private static final int ARTIFACT_SIZE = 5 * 1024 * 1024; - private static volatile int downLoadProgress = 0; private static volatile long shippedBytes = 0; @@ -84,9 +82,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { assignDistributionSet(ds, targets); // create artifact - final byte random[] = RandomUtils.nextBytes(5 * 1024); + final int artifactSize = 5 * 1024; + final byte random[] = RandomUtils.nextBytes(artifactSize); final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), - ds.findFirstModuleByType(osType).get().getId(), "file1", false); + ds.findFirstModuleByType(osType).get().getId(), "file1", false, artifactSize); // no artifact available mvc.perform(get("/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/123455", @@ -170,9 +169,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { final DistributionSet ds = testdataFactory.createDistributionSet(""); // create artifact - final byte random[] = RandomUtils.nextBytes(ARTIFACT_SIZE); + final int artifactSize = 5 * 1024 * 1024; + final byte random[] = RandomUtils.nextBytes(artifactSize); final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), - ds.findFirstModuleByType(osType).get().getId(), "file1", false); + 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}", @@ -180,11 +180,9 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { // 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())) + 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())))) @@ -196,7 +194,7 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { // download complete assertThat(downLoadProgress).isEqualTo(10); - assertThat(shippedBytes).isEqualTo(ARTIFACT_SIZE); + assertThat(shippedBytes).isEqualTo(artifactSize); } @Test @@ -211,16 +209,15 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { assignDistributionSet(ds, target); // create artifact - final byte random[] = RandomUtils.nextBytes(5 * 1024); + final int artifactSize = 5 * 1024; + final byte random[] = RandomUtils.nextBytes(artifactSize); final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), getOsModule(ds), "file1", - false); + false, artifactSize); // download - final MvcResult result = mvc - .perform( - get("/{tenant}/controller/v1/{controllerId}/softwaremodules/{softwareModuleId}/artifacts/{filename}.MD5SUM", - tenantAware.getCurrentTenant(), target.getControllerId(), getOsModule(ds), - artifact.getFilename())) + 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(); @@ -245,7 +242,7 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { // create artifact final byte random[] = RandomUtils.nextBytes(resultLength); final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), getOsModule(ds), "file1", - false); + false, resultLength); assertThat(random.length).isEqualTo(resultLength); @@ -259,11 +256,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { 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)) + 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")) @@ -278,11 +274,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { 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")) + 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")) @@ -296,11 +291,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { .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-")) + 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")) @@ -326,11 +320,10 @@ public class DdiArtifactDownloadTest extends AbstractDDiApiIntegrationTest { .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")) + 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")) diff --git a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java index 2033f07ae..d9d3de514 100644 --- a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java +++ b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java @@ -21,6 +21,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.util.HashMap; import java.util.Map; +import org.eclipse.hawkbit.exception.SpServerError; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.rest.util.JsonBuilder; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; @@ -109,13 +111,13 @@ public class DdiConfigDataTest extends AbstractDDiApiIntegrationTest { @Test @Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) " - + "upload limitation is inplace which is meant to protect the server from malicious attempts.") - public void putToMuchConfigData() throws Exception { + + "upload quota is enforced to protect the server from malicious attempts.") + public void putTooMuchConfigData() throws Exception { testdataFactory.createTarget("4717"); // initial Map attributes = new HashMap<>(); - for (int i = 0; i < 10; i++) { + for (int i = 0; i < quotaManagement.getMaxAttributeEntriesPerTarget(); i++) { attributes.put("dsafsdf" + i, "sdsds" + i); } mvc.perform(put("/{tenant}/controller/v1/4717/configData", tenantAware.getCurrentTenant()) @@ -126,7 +128,9 @@ public class DdiConfigDataTest extends AbstractDDiApiIntegrationTest { attributes.put("on too many", "sdsds"); mvc.perform(put("/{tenant}/controller/v1/4717/configData", tenantAware.getCurrentTenant()) .content(JsonBuilder.configData("", attributes, "closed")).contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isForbidden()); + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); } @@ -192,8 +196,7 @@ public class DdiConfigDataTest extends AbstractDDiApiIntegrationTest { } @Step - private void putConfigDataWithInvalidUpdateMode(final String configDataPath) - throws Exception { + private void putConfigDataWithInvalidUpdateMode(final String configDataPath) throws Exception { // create some attriutes final Map attributes = new HashMap<>(); diff --git a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java index cf6a354ca..25034c562 100644 --- a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java +++ b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java @@ -110,11 +110,12 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final DistributionSet ds = testdataFactory.createDistributionSet("", true); final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true); - final byte random[] = RandomUtils.nextBytes(5 * 1024); + final int artifactSize = 5 * 1024; + final byte random[] = RandomUtils.nextBytes(artifactSize); final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), getOsModule(ds), "test1", - false); + false, artifactSize); final Artifact artifactSignature = artifactManagement.create(new ByteArrayInputStream(random), getOsModule(ds), - "test1.signature", false); + "test1.signature", false, artifactSize); final Target savedTarget = testdataFactory.createTarget("4712"); @@ -198,10 +199,9 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { .andExpect(jsonPath("$.deployment.chunks[?(@.part==os)].artifacts[1].hashes.sha1", contains(artifactSignature.getSha1Hash()))) - .andExpect( - jsonPath("$.deployment.chunks[?(@.part==os)].artifacts[1]._links.download-http.href", - contains(HTTP_LOCALHOST + tenantAware.getCurrentTenant() - + "/controller/v1/4712/softwaremodules/" + .andExpect(jsonPath("$.deployment.chunks[?(@.part==os)].artifacts[1]._links.download-http.href", + contains( + HTTP_LOCALHOST + tenantAware.getCurrentTenant() + "/controller/v1/4712/softwaremodules/" + findDistributionSetByAction.findFirstModuleByType(osType).get().getId() + "/artifacts/test1.signature"))) .andExpect(jsonPath("$.deployment.chunks[?(@.part==os)].artifacts[1]._links.md5sum-http.href", @@ -268,11 +268,12 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final String visibleMetadataOsKey = "metaDataVisible"; final String visibleMetadataOsValue = "withValue"; - final byte random[] = RandomUtils.nextBytes(5 * 1024); + final int artifactSize = 5 * 1024; + final byte random[] = RandomUtils.nextBytes(artifactSize); final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), getOsModule(ds), "test1", - false); + false, artifactSize); final Artifact artifactSignature = artifactManagement.create(new ByteArrayInputStream(random), getOsModule(ds), - "test1.signature", false); + "test1.signature", false, artifactSize); softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(getOsModule(ds)) .key(visibleMetadataOsKey).value(visibleMetadataOsValue).targetVisible(true)); @@ -391,11 +392,12 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest { final DistributionSet ds = testdataFactory.createDistributionSet("", true); final DistributionSet ds2 = testdataFactory.createDistributionSet("2", true); - final byte random[] = RandomUtils.nextBytes(5 * 1024); + final int artifactSize = 5 * 1024; + final byte random[] = RandomUtils.nextBytes(artifactSize); final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), getOsModule(ds), "test1", - false); + false, artifactSize); final Artifact artifactSignature = artifactManagement.create(new ByteArrayInputStream(random), getOsModule(ds), - "test1.signature", false); + "test1.signature", false, artifactSize); final Target savedTarget = testdataFactory.createTarget("4712"); diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java index f26e734ac..95137b1ba 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRestModelMapper.java @@ -46,11 +46,13 @@ final class MgmtRestModelMapper { } /** - * Convert a action rest type to a action repository type. + * Convert the given {@link MgmtActionType} into a corresponding repository + * {@link ActionType}. * * @param actionTypeRest - * the rest type - * @return or the action repository type + * the REST representation of the action type + * + * @return or the repository action type */ public static ActionType convertActionType(final MgmtActionType actionTypeRest) { if (actionTypeRest == null) { @@ -70,11 +72,13 @@ final class MgmtRestModelMapper { } /** - * Convert a action repository type to rest type. + * Converts the given repository {@link ActionType} into a corresponding + * {@link MgmtActionType}. * * @param actionType - * the rest type - * @return or the action repository type + * the repository representation of the action type + * + * @return or the REST action type */ public static MgmtActionType convertActionType(final ActionType actionType) { if (actionType == null) { diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResource.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResource.java index 76dda3f28..5e51c6f2c 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResource.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResource.java @@ -83,7 +83,7 @@ public class MgmtSoftwareModuleResource implements MgmtSoftwareModuleRestApi { try (InputStream in = file.getInputStream()) { final Artifact result = artifactManagement.create(in, softwareModuleId, fileName, md5Sum == null ? null : md5Sum.toLowerCase(), sha1Sum == null ? null : sha1Sum.toLowerCase(), false, - file.getContentType()); + file.getContentType(), file.getSize()); final MgmtArtifact reponse = MgmtSoftwareModuleMapper.toResponse(result); MgmtSoftwareModuleMapper.addLinks(result, reponse); diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index 4235de1b6..e18dc2260 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -27,9 +27,12 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.stream.IntStream; import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; @@ -44,9 +47,11 @@ import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.json.JSONArray; import org.json.JSONObject; import org.junit.Test; +import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; +import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.jayway.jsonpath.JsonPath; @@ -179,6 +184,45 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr mvc.perform(get(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM")) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) .andExpect(jsonPath("$.size", equalTo(smIDs.size()))); + + // verify quota enforcement + final int maxSoftwareModules = quotaManagement.getMaxSoftwareModulesPerDistributionSet(); + final List moduleIDs = Lists.newArrayList(); + for (int i = 0; i < maxSoftwareModules + 1; ++i) { + moduleIDs.add(testdataFactory.createSoftwareModuleApp("sm" + i).getId()); + } + + // post assignment + final String jsonIDs = JsonBuilder.ids(moduleIDs.subList(0, maxSoftwareModules - smIDs.size())); + mvc.perform(post(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM") + .contentType(MediaType.APPLICATION_JSON).content(jsonIDs)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + // test if size corresponds with quota + mvc.perform(get(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + + "/assignedSM?limit={limit}", maxSoftwareModules * 2)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()).andExpect(jsonPath("$.size", equalTo(maxSoftwareModules))); + + // post one more to cause the quota to be exceeded + mvc.perform(post(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet.getId() + "/assignedSM") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonBuilder.ids(Collections.singletonList(moduleIDs.get(moduleIDs.size() - 1))))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); + + // verify quota is also enforced for bulk uploads + final DistributionSet disSet2 = testdataFactory.createDistributionSetWithNoSoftwareModules("Saturn", "4.0"); + mvc.perform(post(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet2.getId() + "/assignedSM") + .contentType(MediaType.APPLICATION_JSON).content(JsonBuilder.ids(moduleIDs))) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); + + // verify size is still 0 + mvc.perform(get(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + disSet2.getId() + "/assignedSM")) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.size", equalTo(0))); + } @Test @@ -229,6 +273,48 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr .as("Five targets in repository have DS assigned").hasSize(5); } + @Test + @Description("Ensures that multi target assignment is protected by our 'max targets per manual assignment' quota.") + public void assignMultipleTargetsToDistributionSetUntilQuotaIsExceeded() throws Exception { + + final int maxTargets = quotaManagement.getMaxTargetsPerManualAssignment(); + final List targets = testdataFactory.createTargets(maxTargets + 1); + final DistributionSet ds = testdataFactory.createDistributionSet(); + + final JSONArray payload = new JSONArray(); + targets.forEach(trg -> payload.put(new JSONObject().put("id", trg.getId()))); + + mvc.perform(post(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + ds.getId() + "/assignedTargets") + .contentType(MediaType.APPLICATION_JSON).content(payload.toString())).andExpect(status().isForbidden()); + + assertThat(targetManagement.findByAssignedDistributionSet(PAGE, ds.getId()).getContent()).isEmpty(); + } + + @Test + @Description("Ensures that the 'max actions per target' quota is enforced if the distribution set assignment of a target is changed permanently") + public void changeDistributionSetAssignmentForTargetUntilQuotaIsExceeded() throws Exception { + + // create one target + final Target testTarget = testdataFactory.createTarget("trg1"); + final int maxActions = quotaManagement.getMaxActionsPerTarget(); + + // create a set of distribution sets + final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); + final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); + final DistributionSet ds3 = testdataFactory.createDistributionSet("ds3"); + + IntStream.range(0, maxActions).forEach(i -> { + // toggle the distribution set + assignDistributionSet(i % 2 == 0 ? ds1 : ds2, testTarget); + }); + + // assign our test target to another distribution set and verify that + // the 'max actions per target' quota is exceeded + final String json = new JSONArray().put(new JSONObject().put("id", testTarget.getControllerId())).toString(); + mvc.perform(post(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + ds3.getId() + "/assignedTargets") + .contentType(MediaType.APPLICATION_JSON).content(json)).andExpect(status().isForbidden()); + } + @Test @Description("Ensures that offline reported multi target assignment through API is reflected by the repository.") public void offlineAssignmentOfMultipleTargetsToDistributionSet() throws Exception { @@ -433,14 +519,13 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final Set createDistributionSetsAlphabetical = createDistributionSetsAlphabetical(1); final DistributionSet createdDs = createDistributionSetsAlphabetical.iterator().next(); - targetFilterQueryManagement.updateAutoAssignDS( - targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(knownFilterName).query("x==y")).getId(), + targetFilterQueryManagement.updateAutoAssignDS(targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name(knownFilterName).query("name==y")).getId(), createdDs.getId()); // create some dummy target filter queries - targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("b").query("x==y")); - targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("c").query("x==y")); + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("b").query("name==y")); + targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name("c").query("name==y")); mvc.perform(get(MgmtRestConstants.DISTRIBUTIONSET_V1_REQUEST_MAPPING + "/" + createdDs.getId() + "/autoAssignTargetFilters")).andExpect(status().isOk()).andExpect(jsonPath("$.size", equalTo(1))) @@ -495,16 +580,16 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr private void prepareTestFilters(final String filterNamePrefix, final DistributionSet createdDs) { // create target filter queries that should be found targetFilterQueryManagement.updateAutoAssignDS(targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "1").query("x==y")).getId(), - createdDs.getId()); + .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "1").query("name==y")) + .getId(), createdDs.getId()); targetFilterQueryManagement.updateAutoAssignDS(targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "2").query("x==y")).getId(), - createdDs.getId()); + .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "2").query("name==y")) + .getId(), createdDs.getId()); // create some dummy target filter queries targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "b").query("x==y")); + .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "b").query("name==y")); targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "c").query("x==y")); + .create(entityFactory.targetFilterQuery().create().name(filterNamePrefix + "c").query("name==y")); } @Test @@ -902,12 +987,12 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr final String knownValue1 = "knownValue1"; final String knownValue2 = "knownValue2"; - final JSONArray jsonArray = new JSONArray(); - jsonArray.put(new JSONObject().put("key", knownKey1).put("value", knownValue1)); - jsonArray.put(new JSONObject().put("key", knownKey2).put("value", knownValue2)); + final JSONArray metaData1 = new JSONArray(); + metaData1.put(new JSONObject().put("key", knownKey1).put("value", knownValue1)); + metaData1.put(new JSONObject().put("key", knownKey2).put("value", knownValue2)); mvc.perform(post("/rest/v1/distributionsets/{dsId}/metadata", testDS.getId()).accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON).content(jsonArray.toString())) + .contentType(MediaType.APPLICATION_JSON).content(metaData1.toString())) .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("[0]key", equalTo(knownKey1))).andExpect(jsonPath("[0]value", equalTo(knownValue1))) @@ -921,6 +1006,25 @@ public class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegr assertThat(metaKey1.getValue()).isEqualTo(knownValue1); assertThat(metaKey2.getValue()).isEqualTo(knownValue2); + + // verify quota enforcement + final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerDistributionSet(); + + final JSONArray metaData2 = new JSONArray(); + for (int i = 0; i < maxMetaData - metaData1.length() + 1; ++i) { + metaData2.put(new JSONObject().put("key", knownKey1 + i).put("value", knownValue1 + i)); + } + + mvc.perform(post("/rest/v1/distributionsets/{dsId}/metadata", testDS.getId()).accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON).content(metaData2.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()); + + // verify that the number of meta data entries has not changed + // (we cannot use the PAGE constant here as it tries to sort by ID) + assertThat(distributionSetManagement + .findMetaDataByDistributionSetId(new PageRequest(0, Integer.MAX_VALUE), testDS.getId()) + .getTotalElements()).isEqualTo(metaData1.length()); + } @Test diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java index bbcee4d2b..ef6b572d9 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTypeResourceTest.java @@ -27,7 +27,11 @@ import java.util.Collections; import java.util.List; import org.apache.commons.lang3.RandomStringUtils; +import org.assertj.core.util.Lists; +import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.repository.builder.SoftwareModuleTypeCreate; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.test.util.WithUser; @@ -239,6 +243,60 @@ public class MgmtDistributionSetTypeResourceTest extends AbstractManagementApiIn } + @Test + @WithUser(principal = "uploadTester", allSpPermissions = true) + @Description("Verifies quota enforcement for /rest/v1/distributionsettypes/{ID}/optionalmoduletypes POST requests.") + public void assignModuleTypesToDistributionSetTypeUntilQuotaExceeded() throws Exception { + + // create software module types + final int maxSoftwareModuleTypes = quotaManagement.getMaxSoftwareModuleTypesPerDistributionSetType(); + final List moduleTypeIds = Lists.newArrayList(); + for (int i = 0; i < maxSoftwareModuleTypes + 1; ++i) { + final SoftwareModuleTypeCreate smCreate = entityFactory.softwareModuleType().create().name("smType_" + i) + .description("smType_" + i).maxAssignments(1).colour("blue").key("smType_" + i); + moduleTypeIds.add(softwareModuleTypeManagement.create(smCreate).getId()); + } + + // verify quota enforcement for optional module types + + final DistributionSetType testType = distributionSetTypeManagement.create(entityFactory.distributionSetType() + .create().key("testType").name("testType").description("testType").colour("col12")); + assertThat(testType.getOptLockRevision()).isEqualTo(1); + + for (int i = 0; i < moduleTypeIds.size() - 1; ++i) { + mvc.perform(post("/rest/v1/distributionsettypes/{dstID}/optionalmoduletypes", testType.getId()) + .content("{\"id\":" + moduleTypeIds.get(i) + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + } + + mvc.perform(post("/rest/v1/distributionsettypes/{dstID}/optionalmoduletypes", testType.getId()) + .content("{\"id\":" + moduleTypeIds.get(moduleTypeIds.size() - 1) + "}") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); + + // verify quota enforcement for mandatory module types + + final DistributionSetType testType2 = distributionSetTypeManagement.create(entityFactory.distributionSetType() + .create().key("testType2").name("testType2").description("testType2").colour("col12")); + assertThat(testType2.getOptLockRevision()).isEqualTo(1); + + for (int i = 0; i < moduleTypeIds.size() - 1; ++i) { + mvc.perform(post("/rest/v1/distributionsettypes/{dstID}/mandatorymoduletypes", testType2.getId()) + .content("{\"id\":" + moduleTypeIds.get(i) + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + } + + mvc.perform(post("/rest/v1/distributionsettypes/{dstID}/mandatorymoduletypes", testType2.getId()) + .content("{\"id\":" + moduleTypeIds.get(moduleTypeIds.size() - 1) + "}") + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); + + } + @Test @WithUser(principal = "uploadTester", allSpPermissions = true) @Description("Checks the correct behaviour of /rest/v1/distributionsettypes/{ID}/mandatorymoduletypes GET requests.") diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index 1c5d3a839..2f71c0e98 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -27,9 +27,11 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; +import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; @@ -131,9 +133,45 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes postRollout("rollout1", 10, dsA.getId(), "id==target*", 20); } + @Test + @Description("Verifies that rollout cannot be created if too many rollout groups are specified.") + public void createRolloutWithTooManyRolloutGroups() throws Exception { + + final int maxGroups = quotaManagement.getMaxRolloutGroupsPerRollout(); + testdataFactory.createTargets(20, "target", "rollout"); + + mvc.perform(post("/rest/v1/rollouts") + .content(JsonBuilder.rollout("rollout1", "rollout1Desc", maxGroups + 1, + testdataFactory.createDistributionSet("ds").getId(), "id==target*", + new RolloutGroupConditionBuilder().withDefaults().build())) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); + + } + + @Test + @Description("Verifies that rollout cannot be created if the 'max targets per rollout group' quota would be violated for one of the groups.") + public void createRolloutFailsIfRolloutGroupQuotaIsViolated() throws Exception { + + final int maxTargets = quotaManagement.getMaxTargetsPerRolloutGroup(); + testdataFactory.createTargets(maxTargets + 1, "target", "rollout"); + + mvc.perform(post("/rest/v1/rollouts") + .content(JsonBuilder.rollout("rollout1", "rollout1Desc", 1, + testdataFactory.createDistributionSet("ds").getId(), "id==target*", + new RolloutGroupConditionBuilder().withDefaults().build())) + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); + + } + @Test @Description("Testing that rollout can be created with groups") - public void createRolloutWithGroupsDefinitions() throws Exception { + public void createRolloutWithGroupDefinitions() throws Exception { final DistributionSet dsA = testdataFactory.createDistributionSet("ro"); final int amountTargets = 10; @@ -160,7 +198,7 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes @Test @Description("Testing that no rollout with groups that have illegal percentages can be created") - public void createRolloutWithToLowlPercentage() throws Exception { + public void createRolloutWithTooLowPercentage() throws Exception { final DistributionSet dsA = testdataFactory.createDistributionSet("ro2"); final int amountTargets = 10; @@ -185,7 +223,7 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes @Test @Description("Testing that no rollout with groups that have illegal percentages can be created") - public void createRolloutWithToHighPercentage() throws Exception { + public void createRolloutWithTooHighPercentage() throws Exception { final DistributionSet dsA = testdataFactory.createDistributionSet("ro2"); final int amountTargets = 10; @@ -217,7 +255,7 @@ public class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTes } @Test - @Description("Terives sinle rollout from management API includinfg extra data that is delieverd only for single rollout access.") + @Description("Retrieves single rollout from management API including extra data that is delivered only for single rollout access.") public void retrieveSingleRollout() throws Exception { testdataFactory.createTargets(20, "rollout", "rollout"); final DistributionSet dsA = testdataFactory.createDistributionSet(""); diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java index 2c06d64e3..47b2f74ad 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtSoftwareModuleResourceTest.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; +import java.util.Random; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; @@ -36,6 +37,7 @@ import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.mgmt.json.model.artifact.MgmtArtifact; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.Constants; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.SoftwareModule; @@ -49,8 +51,10 @@ import org.json.JSONArray; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; +import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MvcResult; import org.springframework.web.bind.annotation.RestController; @@ -66,6 +70,7 @@ import ru.yandex.qatools.allure.annotations.Stories; */ @Features("Component Tests - Management API") @Stories("Software Module Resource") +@TestPropertySource(properties = { "hawkbit.server.security.dos.maxArtifactSize=100000" }) public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegrationTest { @Before @@ -147,7 +152,7 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra } @Test - @Description("Tests the uppload of an artifact binary. The upload is executed and the content checked in the repository for completenes.") + @Description("Tests the upload of an artifact binary. The upload is executed and the content checked in the repository for completeness.") public void uploadArtifact() throws Exception { final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); @@ -183,6 +188,24 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra assertArtifact(sm, random); } + @Test + @Description("Verifies that artifacts which exceed the configured maximum size cannot be uploaded.") + public void uploadArtifactFailsIfTooLarge() throws Exception { + final SoftwareModule sm = testdataFactory.createSoftwareModule("quota", "quota"); + final long maxSize = quotaManagement.getMaxArtifactSize(); + + // create a file which exceeds the configured maximum size + final byte[] randomBytes = new byte[Math.toIntExact(maxSize) + 1024]; + new Random().nextBytes(randomBytes); + + final MockMultipartFile file = new MockMultipartFile("file", "origFilename", null, randomBytes); + + // try to upload + mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isForbidden()); + } + private void assertArtifact(final SoftwareModule sm, final byte[] random) throws IOException { // check result in db... // repo @@ -312,17 +335,57 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra } + @Test + @Description("Verifies that only a limited number of artifacts can be uploaded for one software module.") + public void uploadArtifactsUntilQuotaExceeded() throws Exception { + final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); + final long maxArtifacts = quotaManagement.getMaxArtifactsPerSoftwareModule(); + + for (int i = 0; i < maxArtifacts; ++i) { + // create test file + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final String md5sum = HashGeneratorUtils.generateMD5(random); + final String sha1sum = HashGeneratorUtils.generateSHA1(random); + final MockMultipartFile file = new MockMultipartFile("file", "origFilename" + i, null, random); + + // upload + mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.hashes.md5", equalTo(md5sum))) + .andExpect(jsonPath("$.hashes.sha1", equalTo(sha1sum))) + .andExpect(jsonPath("$.size", equalTo(random.length))) + .andExpect(jsonPath("$.providedFilename", equalTo("origFilename" + i))).andReturn(); + } + + // upload one more file to cause the quota to be exceeded + final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + HashGeneratorUtils.generateMD5(random); + HashGeneratorUtils.generateSHA1(random); + final MockMultipartFile file = new MockMultipartFile("file", "origFilename_final", null, random); + + // upload + mvc.perform(fileUpload("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).file(file) + .accept(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.exceptionClass", equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath("$.errorCode", equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); + + } + @Test @Description("Tests binary download of an artifact including verfication that the downloaded binary is consistent and that the etag header is as expected identical to the SHA1 hash of the file.") public void downloadArtifact() throws Exception { final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", - false); + false, artifactSize); final Artifact artifact2 = artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file2", - false); + false, artifactSize); downloadAndVerify(sm, random, artifact); downloadAndVerify(sm, random, artifact2); @@ -348,9 +411,11 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra // prepare data for test final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); + final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", - false); + false, artifactSize); // perform test mvc.perform(get("/rest/v1/softwaremodules/{smId}/artifacts/{artId}", sm.getId(), artifact.getId()) @@ -373,12 +438,13 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra public void getArtifacts() throws Exception { final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", - false); + false, artifactSize); final Artifact artifact2 = artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file2", - false); + false, artifactSize); mvc.perform(get("/rest/v1/softwaremodules/{smId}/artifacts", sm.getId()).accept(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) @@ -402,7 +468,9 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra @Test @Description("Verfies that the system refuses unsupported request types and answers as defined to them, e.g. NOT FOUND on a non existing resource. Or a HTTP POST for updating a resource results in METHOD NOT ALLOWED etc.") public void invalidRequestsOnArtifactResource() throws Exception { - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); final MockMultipartFile file = new MockMultipartFile("file", "orig", null, random); final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); @@ -415,7 +483,7 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra .andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound()); // SM does not exist - artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", false); + artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", false, artifactSize); mvc.perform(get("/rest/v1/softwaremodules/1234567890/artifacts")).andDo(MockMvcResultPrinter.print()) .andExpect(status().isNotFound()); @@ -723,9 +791,10 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); - artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", false); + artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", false, artifactSize); assertThat(softwareModuleManagement.findAll(PAGE)).as("Softwaremoudle size is wrong").hasSize(1); assertThat(artifactManagement.count()).isEqualTo(1); @@ -743,11 +812,12 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra public void deleteAssignedSoftwareModule() throws Exception { final DistributionSet ds1 = testdataFactory.createDistributionSet("a"); - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); final Long appTypeSmId = ds1.findFirstModuleByType(appType).get().getId(); - artifactManagement.create(new ByteArrayInputStream(random), appTypeSmId, "file1", false); + artifactManagement.create(new ByteArrayInputStream(random), appTypeSmId, "file1", false, artifactSize); assertThat(softwareModuleManagement.count()).isEqualTo(3); assertThat(artifactManagement.count()).isEqualTo(1); @@ -771,12 +841,13 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra // Create 1 SM final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); // Create 2 artifacts final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", - false); - artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file2", false); + false, artifactSize); + artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file2", false, artifactSize); // check repo before delete assertThat(softwareModuleManagement.findAll(PAGE)).hasSize(1); @@ -797,7 +868,7 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra } @Test - @Description("Verfies the successfull creation of metadata.") + @Description("Verfies the successful creation of metadata and the enforcement of the meta data quota.") public void createMetadata() throws Exception { final String knownKey1 = "knownKey1"; @@ -807,12 +878,12 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); - final JSONArray jsonArray = new JSONArray(); - jsonArray.put(new JSONObject().put("key", knownKey1).put("value", knownValue1)); - jsonArray.put(new JSONObject().put("key", knownKey2).put("value", knownValue2).put("targetVisible", true)); + final JSONArray metaData1 = new JSONArray(); + metaData1.put(new JSONObject().put("key", knownKey1).put("value", knownValue1)); + metaData1.put(new JSONObject().put("key", knownKey2).put("value", knownValue2).put("targetVisible", true)); mvc.perform(post("/rest/v1/softwaremodules/{swId}/metadata", sm.getId()).accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON).content(jsonArray.toString())) + .contentType(MediaType.APPLICATION_JSON).content(metaData1.toString())) .andDo(MockMvcResultPrinter.print()).andExpect(status().isCreated()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("[0]key", equalTo(knownKey1))).andExpect(jsonPath("[0]value", equalTo(knownValue1))) @@ -827,6 +898,25 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra assertThat(metaKey1.getValue()).as("Metadata key is wrong").isEqualTo(knownValue1); assertThat(metaKey2.getValue()).as("Metadata key is wrong").isEqualTo(knownValue2); + + // verify quota enforcement + final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerSoftwareModule(); + + final JSONArray metaData2 = new JSONArray(); + for (int i = 0; i < maxMetaData - metaData1.length() + 1; ++i) { + metaData2.put(new JSONObject().put("key", knownKey1 + i).put("value", knownValue1 + i)); + } + + mvc.perform(post("/rest/v1/softwaremodules/{swId}/metadata", sm.getId()).accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON).content(metaData2.toString())) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()); + + // verify that the number of meta data entries has not changed + // (we cannot use the PAGE constant here as it tries to sort by ID) + assertThat(softwareModuleManagement + .findMetaDataBySoftwareModuleId(new PageRequest(0, Integer.MAX_VALUE), sm.getId()).getTotalElements()) + .isEqualTo(metaData1.length()); + } @Test @@ -930,4 +1020,5 @@ public class MgmtSoftwareModuleResourceTest extends AbstractManagementApiIntegra character++; } } + } diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java index 9ccf8ebc6..a77ba3ba9 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetFilterQueryResourceTest.java @@ -21,6 +21,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.rest.exception.MessageNotReadableException; @@ -53,6 +54,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte private static final String JSON_PATH_FIELD_SIZE = ".size"; private static final String JSON_PATH_FIELD_TOTAL = ".total"; private static final String JSON_PATH_FIELD_AUTO_ASSIGN_DS = ".autoAssignDistributionSet"; + private static final String JSON_PATH_FIELD_EXCEPTION_CLASS = ".exceptionClass"; + private static final String JSON_PATH_FIELD_ERROR_CODE = ".errorCode"; // target // $.field @@ -64,6 +67,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte private static final String JSON_PATH_ID = JSON_PATH_ROOT + JSON_PATH_FIELD_ID; private static final String JSON_PATH_QUERY = JSON_PATH_ROOT + JSON_PATH_FIELD_QUERY; private static final String JSON_PATH_AUTO_ASSIGN_DS = JSON_PATH_ROOT + JSON_PATH_FIELD_AUTO_ASSIGN_DS; + private static final String JSON_PATH_EXCEPTION_CLASS = JSON_PATH_ROOT + JSON_PATH_FIELD_EXCEPTION_CLASS; + private static final String JSON_PATH_ERROR_CODE = JSON_PATH_ROOT + JSON_PATH_FIELD_ERROR_CODE; @Test @Description("Ensures that deletion is executed if permitted.") @@ -126,8 +131,8 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte final String body = new JSONObject().put("name", filterName2).toString(); // prepare - final TargetFilterQuery tfq = targetFilterQueryManagement.create( - entityFactory.targetFilterQuery().create().name(filterName).query(filterQuery)); + final TargetFilterQuery tfq = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name(filterName).query(filterQuery)); mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId()).content(body) .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()) @@ -230,6 +235,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte } @Test + @Description("Ensures that a single target filter query can be retrieved via its id.") public void getSingleTarget() throws Exception { // create first a target which can be retrieved by rest interface final String knownQuery = "name=test01"; @@ -247,6 +253,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte } @Test + @Description("Ensures that the retrieval of a non-existing target filter query results in a HTTP Not found error (404).") public void getSingleTargetNoExistsResponseNotFound() throws Exception { final String targetIdNotExists = "546546"; // test @@ -262,6 +269,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte } @Test + @Description("Ensures that the creation of a target filter query based on an invalid request payload results in a HTTP Bad Request error (400).") public void createTargetFilterQueryWithBadPayloadBadRequest() throws Exception { final String notJson = "abc"; @@ -279,22 +287,72 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte assertThat(exceptionInfo.getErrorCode()).isEqualTo(SpServerError.SP_REST_BODY_NOT_READABLE.getKey()); } + @Test + @Description("Ensures that the assignment of an auto-assign distribution set results in a HTTP Forbidden error (403) " + + "if the (existing) query addresses too many targets.") + public void setAutoAssignDistributionSetOnFilterQueryThatExceedsQuota() throws Exception { + + // create targets + final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment(); + testdataFactory.createTargets(maxTargets + 1, "target%s"); + + // create the filter query and the distribution set + final DistributionSet set = testdataFactory.createDistributionSet(); + final TargetFilterQuery filterQuery = createSingleTargetFilterQuery("1", "controllerId==target*"); + + mvc.perform( + post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId() + "/autoAssignDS") + .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); + } + + @Test + @Description("Ensures that the update of a target filter query results in a HTTP Forbidden error (403) " + + "if the updated query addresses too many targets.") + public void updateTargetFilterQueryWithQueryThatExceedsQuota() throws Exception { + + // create targets + final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment(); + testdataFactory.createTargets(maxTargets + 1, "target%s"); + + // create the filter query and the distribution set + final DistributionSet set = testdataFactory.createDistributionSet(); + final TargetFilterQuery filterQuery = createSingleTargetFilterQuery("1", "controllerId==target1"); + + // assign the auto-assign distribution set, this should work + mvc.perform( + post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId() + "/autoAssignDS") + .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + assertThat(targetFilterQueryManagement.get(filterQuery.getId()).get().getAutoAssignDistributionSet()) + .isEqualTo(set); + + // update the query of the filter query to trigger a quota hit + mvc.perform(put(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + filterQuery.getId()) + .content("{\"query\":\"controllerId==target*\"}").contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isForbidden()) + .andExpect(jsonPath(JSON_PATH_EXCEPTION_CLASS, equalTo(QuotaExceededException.class.getName()))) + .andExpect(jsonPath(JSON_PATH_ERROR_CODE, equalTo(SpServerError.SP_QUOTA_EXCEEDED.getKey()))); + + } + @Test public void setAutoAssignDistributionSetToTargetFilterQuery() throws Exception { - final String knownQuery = "name=test05"; + final String knownQuery = "name==test05"; final String knownName = "filter05"; - final DistributionSet set = testdataFactory.createDistributionSet("one"); + final DistributionSet set = testdataFactory.createDistributionSet(); final TargetFilterQuery tfq = createSingleTargetFilterQuery(knownName, knownQuery); mvc.perform(post(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS") .content("{\"id\":" + set.getId() + "}").contentType(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); - assertThat( - targetFilterQueryManagement.get(tfq.getId()).get().getAutoAssignDistributionSet()) - .isEqualTo(set); + assertThat(targetFilterQueryManagement.get(tfq.getId()).get().getAutoAssignDistributionSet()).isEqualTo(set); final String hrefPrefix = "http://localhost" + MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId(); @@ -311,7 +369,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte @Test public void deleteAutoAssignDistributionSetOfTargetFilterQuery() throws Exception { - final String knownQuery = "name=test06"; + final String knownQuery = "name==test06"; final String knownName = "filter06"; final String dsName = "testDS"; @@ -319,9 +377,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte final TargetFilterQuery tfq = createSingleTargetFilterQuery(knownName, knownQuery); targetFilterQueryManagement.updateAutoAssignDS(tfq.getId(), set.getId()); - assertThat( - targetFilterQueryManagement.get(tfq.getId()).get().getAutoAssignDistributionSet()) - .isEqualTo(set); + assertThat(targetFilterQueryManagement.get(tfq.getId()).get().getAutoAssignDistributionSet()).isEqualTo(set); mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS")) .andExpect(status().isOk()).andExpect(jsonPath(JSON_PATH_NAME, equalTo(dsName))); @@ -329,9 +385,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte mvc.perform(delete(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS")) .andExpect(status().isNoContent()); - assertThat( - targetFilterQueryManagement.get(tfq.getId()).get().getAutoAssignDistributionSet()) - .isNull(); + assertThat(targetFilterQueryManagement.get(tfq.getId()).get().getAutoAssignDistributionSet()).isNull(); mvc.perform(get(MgmtRestConstants.TARGET_FILTER_V1_REQUEST_MAPPING + "/" + tfq.getId() + "/autoAssignDS")) .andExpect(status().isNoContent()); @@ -339,8 +393,7 @@ public class MgmtTargetFilterQueryResourceTest extends AbstractManagementApiInte } private TargetFilterQuery createSingleTargetFilterQuery(final String name, final String query) { - return targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name(name).query(query)); + return targetFilterQueryManagement.create(entityFactory.targetFilterQuery().create().name(name).query(query)); } } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java index 8e97df774..02822011b 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ArtifactManagement.java @@ -54,6 +54,8 @@ public interface ArtifactManagement { * @param overrideExisting * to true if the artifact binary can be overridden * if it already exists + * @param filesize + * the size of the file in bytes. * * @return uploaded {@link Artifact} * @@ -64,7 +66,7 @@ public interface ArtifactManagement { */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) Artifact create(@NotNull InputStream inputStream, long moduleId, final String filename, - final boolean overrideExisting); + final boolean overrideExisting, final long filesize); /** * Persists artifact binary as provided by given InputStream. assign the @@ -85,6 +87,9 @@ public interface ArtifactManagement { * if it already exists * @param contentType * the contentType of the file + * @param filesize + * the size of the file in bytes. + * * @return uploaded {@link Artifact} * * @throws EntityNotFoundException @@ -100,7 +105,7 @@ public interface ArtifactManagement { */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY) Artifact create(@NotNull InputStream stream, long moduleId, @NotEmpty String filename, String providedMd5Sum, - String providedSha1Sum, boolean overrideExisting, String contentType); + String providedSha1Sum, boolean overrideExisting, String contentType, long filesize); /** * Garbage collects artifact binaries if only referenced by given diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java index 6722aaa6e..ee2a961fd 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -19,6 +19,7 @@ import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEv import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.Action; @@ -45,7 +46,7 @@ import org.springframework.security.access.prepost.PreAuthorize; public interface DeploymentManagement { /** - * method assigns the {@link DistributionSet} to all {@link Target}s by + * Assigns the addressed {@link DistributionSet} to all {@link Target}s by * their IDs with a specific {@link ActionType} and {@code forcetime}. * * @param dsID @@ -66,13 +67,17 @@ public interface DeploymentManagement { * @throws EntityNotFoundException * if either provided {@link DistributionSet} or {@link Target}s * do not exist + * + * @throws QuotaExceededException + * if the maximum number of targets the distribution set can be + * assigned to at once is exceeded */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) DistributionSetAssignmentResult assignDistributionSet(long dsID, @NotNull ActionType actionType, long forcedTimestamp, @NotEmpty Collection controllerIDs); /** - * method assigns the {@link DistributionSet} to all {@link Target}s by + * Assigns the addressed {@link DistributionSet} to all {@link Target}s by * their IDs with a specific {@link ActionType} and {@code forcetime}. * * @param dsID @@ -88,13 +93,17 @@ public interface DeploymentManagement { * @throws EntityNotFoundException * if either provided {@link DistributionSet} or {@link Target}s * do not exist + * + * @throws QuotaExceededException + * if the maximum number of targets the distribution set can be + * assigned to at once is exceeded */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) DistributionSetAssignmentResult assignDistributionSet(long dsID, @NotEmpty Collection targets); /** - * method assigns the {@link DistributionSet} to all {@link Target}s by + * Assigns the addressed {@link DistributionSet} to all {@link Target}s by * their IDs with a specific {@link ActionType} and an action message. * * @param dsID @@ -112,16 +121,20 @@ public interface DeploymentManagement { * @throws EntityNotFoundException * if either provided {@link DistributionSet} or {@link Target}s * do not exist + * + * @throws QuotaExceededException + * if the maximum number of targets the distribution set can be + * assigned to at once is exceeded */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) DistributionSetAssignmentResult assignDistributionSet(long dsID, @NotEmpty Collection targets, String actionMessage); /** - * Method registers an "offline" assignment, i.e. adds a completed action - * for the given {@link DistributionSet} to the given {@link Target}s. + * Registers an "offline" assignment, i.e. adds a completed action for the + * given {@link DistributionSet} to the given {@link Target}s. * - * The handling differs to hawkBit managed updates my means that:
+ * The handling differs to hawkBit-managed updates by means that:
* *
    *
  1. it ignores targets completely that are in @@ -145,15 +158,18 @@ public interface DeploymentManagement { * @throws EntityNotFoundException * if either provided {@link DistributionSet} or {@link Target}s * do not exist + * + * @throws QuotaExceededException + * if the maximum number of targets the distribution set can be + * assigned to at once is exceeded */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET) DistributionSetAssignmentResult offlineAssignedDistributionSet(Long dsID, Collection controllerIDs); /** - * Cancels given {@link Action} for given {@link Target}. The method will - * immediately add a {@link Status#CANCELED} status to the action. However, - * it might be possible that the controller will continue to work on the - * cancellation. + * Cancels the {@link Action} with the given ID. The method will immediately + * add a {@link Status#CANCELED} status to the action. However, it might be + * possible that the controller will continue to work on the cancellation. * * @param actionId * to be canceled @@ -170,7 +186,7 @@ public interface DeploymentManagement { Action cancelAction(long actionId); /** - * counts all actions associated to a specific target. + * Counts all actions associated to a specific target. * * @param rsqlParam * rsql query string @@ -202,7 +218,7 @@ public interface DeploymentManagement { long countActionsAll(); /** - * counts all actions associated to a specific target. + * Counts all actions associated to a specific target. * * @param controllerId * the target associated to the actions to count diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java index 9b9e1ad9a..5b2606a5b 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetManagement.java @@ -21,6 +21,7 @@ import org.eclipse.hawkbit.repository.builder.DistributionSetUpdate; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException; @@ -52,6 +53,7 @@ public interface DistributionSetManagement * to assign and update * @param moduleIds * to get assigned + * * @return the updated {@link DistributionSet}. * * @throws EntityNotFoundException @@ -62,8 +64,12 @@ public interface DistributionSetManagement * the DS is already in use. * * @throws UnsupportedSoftwareModuleForThisDistributionSetException - * is {@link SoftwareModule#getType()} is not supported by this + * if {@link SoftwareModule#getType()} is not supported by this * {@link DistributionSet#getType()}. + * + * @throws QuotaExceededException + * if the maximum number of {@link SoftwareModule}s is exceeded + * for the addressed {@link DistributionSet}. */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) DistributionSet assignSoftwareModules(long setId, @NotEmpty Collection moduleIds); @@ -76,6 +82,7 @@ public interface DistributionSetManagement * to assign for * @param tagId * to assign + * * @return list of assigned ds * * @throws EntityNotFoundException @@ -86,7 +93,7 @@ public interface DistributionSetManagement List assignTag(@NotEmpty Collection setIds, long tagId); /** - * creates a list of distribution set meta data entries. + * Creates a list of distribution set meta data entries. * * @param setId * if the {@link DistributionSet} the metadata has to be created @@ -97,15 +104,20 @@ public interface DistributionSetManagement * * @throws EntityNotFoundException * if given set does not exist + * * @throws EntityAlreadyExistsException * in case one of the meta data entry already exists for the * specific key + * + * @throws QuotaExceededException + * if the maximum number of {@link MetaData} entries is exceeded + * for the addressed {@link DistributionSet} */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) List createMetaData(long setId, @NotEmpty Collection metadata); /** - * deletes a distribution set meta data entry. + * Deletes a distribution set meta data entry. * * @param setId * where meta data has to be deleted @@ -119,7 +131,7 @@ public interface DistributionSetManagement void deleteMetaData(long setId, @NotEmpty String key); /** - * retrieves the distribution set for a given action. + * Retrieves the distribution set for a given action. * * @param actionId * the action associated with the distribution set @@ -140,6 +152,7 @@ public interface DistributionSetManagement * * @param setId * to look for. + * * @return {@link DistributionSet} */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) @@ -152,13 +165,14 @@ public interface DistributionSetManagement * name of {@link DistributionSet}; case insensitive * @param version * version of {@link DistributionSet} + * * @return the page with the found {@link DistributionSet} */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) Optional getByNameAndVersion(@NotEmpty String distributionName, @NotEmpty String version); /** - * finds all meta data by the given distribution set id. + * Finds all meta data by the given distribution set id. * * @param pageable * the page request to page the result @@ -175,7 +189,7 @@ public interface DistributionSetManagement Page findMetaDataByDistributionSetId(@NotNull Pageable pageable, long setId); /** - * finds all meta data by the given distribution set id. + * Finds all meta data by the given distribution set id. * * @param pageable * the page request to page the result @@ -190,6 +204,7 @@ public interface DistributionSetManagement * @throws RSQLParameterUnsupportedFieldException * if a field in the RSQL string is used but not provided by the * given {@code fieldNameProvider} + * * @throws RSQLParameterSyntaxException * if the RSQL syntax is wrong * @@ -201,7 +216,7 @@ public interface DistributionSetManagement @NotNull String rsqlParam); /** - * finds all {@link DistributionSet}s. + * Finds all {@link DistributionSet}s. * * @param pageable * the pagination parameter @@ -210,14 +225,13 @@ public interface DistributionSetManagement * sets or false for only incomplete ones nor * null to return both. * - * * @return all found {@link DistributionSet}s */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) Page findByCompleted(@NotNull Pageable pageable, Boolean complete); /** - * method retrieves all {@link DistributionSet}s from the repository in the + * Method retrieves all {@link DistributionSet}s from the repository in the * following order: *

    * 1) {@link DistributionSet}s which have the given {@link Target} as @@ -235,6 +249,7 @@ public interface DistributionSetManagement * has details of filters to be applied * @param assignedOrInstalled * the id of the Target to be ordered by + * * @return {@link DistributionSet}s */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY) @@ -242,7 +257,7 @@ public interface DistributionSetManagement @NotNull DistributionSetFilterBuilder distributionSetFilterBuilder, @NotEmpty String assignedOrInstalled); /** - * retrieves {@link DistributionSet}s by filtering on the given parameters. + * Retrieves {@link DistributionSet}s by filtering on the given parameters. * * @param pageable * page parameter @@ -255,7 +270,7 @@ public interface DistributionSetManagement @NotNull DistributionSetFilter distributionSetFilter); /** - * retrieves {@link DistributionSet}s by filtering on the given parameters. + * Retrieves {@link DistributionSet}s by filtering on the given parameters. * * @param pageable * page parameter @@ -266,6 +281,7 @@ public interface DistributionSetManagement * @throws RSQLParameterUnsupportedFieldException * if a field in the RSQL string is used but not provided by the * given {@code fieldNameProvider} + * * @throws RSQLParameterSyntaxException * if the RSQL syntax is wrong * @@ -276,7 +292,7 @@ public interface DistributionSetManagement Page findByTag(@NotNull Pageable pageable, long tagId); /** - * retrieves {@link DistributionSet}s by filtering on the given parameters. + * Retrieves {@link DistributionSet}s by filtering on the given parameters. * * @param pageable * page parameter @@ -293,7 +309,7 @@ public interface DistributionSetManagement Page findByRsqlAndTag(@NotNull Pageable pageable, @NotNull String rsqlParam, long tagId); /** - * finds a single distribution set meta data by its id. + * Finds a single distribution set meta data by its id. * * @param setId * of the {@link DistributionSet} @@ -375,7 +391,7 @@ public interface DistributionSetManagement DistributionSet unAssignTag(long setId, long tagId); /** - * updates a distribution set meta data value if corresponding entry exists. + * Updates a distribution set meta data value if corresponding entry exists. * * @param setId * {@link DistributionSet} of the meta data entry to be updated diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeManagement.java index a5d8d1ded..1682b3870 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DistributionSetTypeManagement.java @@ -18,6 +18,7 @@ import org.eclipse.hawkbit.repository.builder.DistributionSetTypeCreate; import org.eclipse.hawkbit.repository.builder.DistributionSetTypeUpdate; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; @@ -63,6 +64,10 @@ public interface DistributionSetTypeManagement * @throws EntityReadOnlyException * if the {@link DistributionSetType} while it is already in use * by a {@link DistributionSet} + * + * @throws QuotaExceededException + * if the maximum number of {@link SoftwareModuleType}s is + * exceeded for the addressed {@link DistributionSetType} */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) DistributionSetType assignOptionalSoftwareModuleTypes(long dsTypeId, @@ -84,6 +89,10 @@ public interface DistributionSetTypeManagement * @throws EntityReadOnlyException * if the {@link DistributionSetType} while it is already in use * by a {@link DistributionSet} + * + * @throws QuotaExceededException + * if the maximum number of {@link SoftwareModuleType}s is + * exceeded for the addressed {@link DistributionSetType} */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) DistributionSetType assignMandatorySoftwareModuleTypes(long dsTypeId, diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java index fa5a133c4..c35265a4c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/QuotaManagement.java @@ -20,7 +20,7 @@ import org.eclipse.hawkbit.repository.model.RolloutGroup; public interface QuotaManagement { /** - * @return Maximum number of {@link ActionStatus} entries that the + * @return maximum number of {@link ActionStatus} entries that the * controller can report for an {@link Action}. */ int getMaxStatusEntriesPerAction(); @@ -43,4 +43,57 @@ public interface QuotaManagement { */ int getMaxMessagesPerActionStatus(); + /** + * @return maximum number of meta data entries per software module + */ + int getMaxMetaDataEntriesPerSoftwareModule(); + + /** + * @return maximum number of meta data entries per distribution set + */ + int getMaxMetaDataEntriesPerDistributionSet(); + + /** + * @return maximum number of software modules per distribution set + */ + int getMaxSoftwareModulesPerDistributionSet(); + + /** + * @return the maximum number of software module types per distribution set + * type + */ + int getMaxSoftwareModuleTypesPerDistributionSetType(); + + /** + * @return the maximum number of artifacts per software module + */ + int getMaxArtifactsPerSoftwareModule(); + + /** + * @return the maximum number of targets per rollout group + */ + int getMaxTargetsPerRolloutGroup(); + + /** + * @return the maximum number of targets which for a manual distribution set + * assignment + */ + int getMaxTargetsPerManualAssignment(); + + /** + * @return the maximum number of targets for an automatic distribution set + * assignment + */ + int getMaxTargetsPerAutoAssignment(); + + /** + * @return the maximum number of actions per target + */ + int getMaxActionsPerTarget(); + + /** + * @return the maximum size of software artifacts in bytes + */ + long getMaxArtifactSize(); + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java index b6e48de7f..1c995b262 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java @@ -22,6 +22,7 @@ import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.builder.RolloutUpdate; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; @@ -123,6 +124,9 @@ public interface RolloutManagement { * if given {@link DistributionSet} does not exist * @throws ConstraintViolationException * if rollout or group parameters are invalid. + * @throws QuotaExceededException + * if the maximum number of allowed targets per rollout group is + * exceeded. */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_CREATE) Rollout create(@NotNull RolloutCreate create, int amountGroup, @NotNull RolloutGroupConditions conditions); @@ -157,6 +161,9 @@ public interface RolloutManagement { * if given {@link DistributionSet} does not exist * @throws ConstraintViolationException * if rollout or group parameters are invalid + * @throws QuotaExceededException + * if the maximum number of allowed targets per rollout group is + * exceeded. */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_CREATE) Rollout create(@NotNull @Valid RolloutCreate rollout, @NotNull @Valid List groups, diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleManagement.java index 6d7819af8..f7465cffd 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SoftwareModuleManagement.java @@ -23,6 +23,7 @@ import org.eclipse.hawkbit.repository.builder.SoftwareModuleMetadataUpdate; import org.eclipse.hawkbit.repository.builder.SoftwareModuleUpdate; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.AssignedSoftwareModule; @@ -60,37 +61,51 @@ public interface SoftwareModuleManagement long countByTextAndType(String searchText, Long typeId); /** - * creates a list of software module meta data entries. + * Creates a list of software module meta data entries. * * @param metadata * the meta data entries to create + * * @return the updated or created software module meta data entries + * * @throws EntityAlreadyExistsException * in case one of the meta data entry already exists for the * specific key + * * @throws EntityNotFoundException * if software module with given ID does not exist + * + * @throws QuotaExceededException + * if the maximum number of {@link SoftwareModuleMetadata} + * entries is exceeded for the addressed {@link SoftwareModule} */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) List createMetaData(@NotNull @Valid Collection metadata); /** - * creates or updates a single software module meta data entry. + * Creates or updates a single software module meta data entry. * * @param metadata * the meta data entry to create + * * @return the updated or created software module meta data entry + * * @throws EntityAlreadyExistsException * in case the meta data entry already exists for the specific * key + * * @throws EntityNotFoundException * if software module with given ID does not exist + * + * @throws QuotaExceededException + * if the maximum number of {@link SoftwareModuleMetadata} + * entries is exceeded for the addressed {@link SoftwareModule} */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_REPOSITORY) SoftwareModuleMetadata createMetaData(@NotNull @Valid SoftwareModuleMetadataCreate metadata); /** - * deletes a software module meta data entry. + * Deletes a software module meta data entry. * * @param moduleId * where meta data has to be deleted @@ -104,12 +119,13 @@ public interface SoftwareModuleManagement void deleteMetaData(long moduleId, @NotEmpty String key); /** - * returns all modules assigned to given {@link DistributionSet}. + * Returns all modules assigned to given {@link DistributionSet}. * * @param pageable * the page request to page the result set * @param setId * to search for + * * @return all {@link SoftwareModule}s that are assigned to given * {@link DistributionSet}. * @@ -130,6 +146,7 @@ public interface SoftwareModuleManagement * to be filtered as "like" on {@link SoftwareModule#getName()} * @param typeId * to be filtered as "like" on {@link SoftwareModule#getType()} + * * @return the page of found {@link SoftwareModule} * * @throws EntityNotFoundException @@ -139,7 +156,7 @@ public interface SoftwareModuleManagement Slice findByTextAndType(@NotNull Pageable pageable, String searchText, Long typeId); /** - * retrieves {@link SoftwareModule} by their name AND version AND type.. + * Retrieves {@link SoftwareModule} by their name AND version AND type.. * * @param name * of the {@link SoftwareModule} @@ -147,6 +164,7 @@ public interface SoftwareModuleManagement * of the {@link SoftwareModule} * @param typeId * of the {@link SoftwareModule} + * * @return the found {@link SoftwareModule} * * @throws EntityNotFoundException @@ -156,7 +174,7 @@ public interface SoftwareModuleManagement Optional getByNameAndVersionAndType(@NotEmpty String name, @NotEmpty String version, long typeId); /** - * finds a single software module meta data by its id. + * Finds a single software module meta data by its id. * * @param moduleId * where meta data has to be found @@ -171,7 +189,7 @@ public interface SoftwareModuleManagement Optional getMetaDataBySoftwareModuleId(long moduleId, @NotEmpty String key); /** - * finds all meta data by the given software module id. + * Finds all meta data by the given software module id. * * @param pageable * the page request to page the result @@ -188,7 +206,7 @@ public interface SoftwareModuleManagement Page findMetaDataBySoftwareModuleId(@NotNull Pageable pageable, long moduleId); /** - * finds all meta data by the given software module id where + * Finds all meta data by the given software module id where * {@link SoftwareModuleMetadata#isTargetVisible()}. * * @param pageable @@ -207,7 +225,7 @@ public interface SoftwareModuleManagement long moduleId); /** - * finds all meta data by the given software module id. + * Finds all meta data by the given software module id. * * @param pageable * the page request to page the result @@ -222,8 +240,10 @@ public interface SoftwareModuleManagement * @throws RSQLParameterUnsupportedFieldException * if a field in the RSQL string is used but not provided by the * given {@code fieldNameProvider} + * * @throws RSQLParameterSyntaxException * if the RSQL syntax is wrong + * * @throws EntityNotFoundException * if software module with given ID does not exist */ @@ -249,6 +269,7 @@ public interface SoftwareModuleManagement * filtered as "like" on {@link SoftwareModule#getName()} * @param typeId * filtered as "equal" on {@link SoftwareModule#getType()} + * * @return the page of found {@link SoftwareModule} * * @throws EntityNotFoundException @@ -259,13 +280,14 @@ public interface SoftwareModuleManagement @NotNull Pageable pageable, long orderByDistributionId, String searchText, Long typeId); /** - * retrieves the {@link SoftwareModule}s by their {@link SoftwareModuleType} + * Retrieves the {@link SoftwareModule}s by their {@link SoftwareModuleType} * . * * @param pageable * page parameters * @param typeId * to be filtered on + * * @return the found {@link SoftwareModule}s * * @throws EntityNotFoundException @@ -275,7 +297,7 @@ public interface SoftwareModuleManagement Slice findByType(@NotNull Pageable pageable, long typeId); /** - * updates a distribution set meta data value if corresponding entry exists. + * Updates a distribution set meta data value if corresponding entry exists. * * @param update * the meta data entry to be updated diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java index e725e05ab..dee780e33 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java @@ -18,8 +18,10 @@ import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryCreate; import org.eclipse.hawkbit.repository.builder.TargetFilterQueryUpdate; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -32,20 +34,26 @@ import org.springframework.security.access.prepost.PreAuthorize; public interface TargetFilterQueryManagement { /** - * creating new {@link TargetFilterQuery}. + * Creates a new {@link TargetFilterQuery}. * * @param create * to create - * @return the created {@link TargetFilterQuery} + * + * @return the new {@link TargetFilterQuery} + * * @throws ConstraintViolationException * if fields are not filled as specified. Check * {@link TargetFilterQueryCreate} for field constraints. + * + * @throws QuotaExceededException + * if the maximum number of targets that is addressed by the + * given query is exceeded (auto-assignments only) */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_TARGET) TargetFilterQuery create(@NotNull @Valid TargetFilterQueryCreate create); /** - * Delete target filter query. + * Deletes the {@link TargetFilterQuery} with the given ID. * * @param targetFilterQueryId * IDs of target filter query to be deleted @@ -57,7 +65,7 @@ public interface TargetFilterQueryManagement { void delete(long targetFilterQueryId); /** - * Verifies provided filter syntax. + * Verifies the provided filter syntax. * * @param query * to verify @@ -67,6 +75,7 @@ public interface TargetFilterQueryManagement { * @throws RSQLParameterUnsupportedFieldException * if a field in the RSQL string is used but not provided by the * given {@code fieldNameProvider} + * * @throws RSQLParameterSyntaxException * if the RSQL syntax is wrong */ @@ -75,7 +84,7 @@ public interface TargetFilterQueryManagement { /** * - * Retrieves all target filter query{@link TargetFilterQuery}. + * Retrieves all {@link TargetFilterQuery}s. * * @param pageable * pagination parameter @@ -85,7 +94,7 @@ public interface TargetFilterQueryManagement { Page findAll(@NotNull Pageable pageable); /** - * Counts all target filter queries + * Counts all {@link TargetFilterQuery}s. * * @return the number of all target filter queries */ @@ -93,46 +102,46 @@ public interface TargetFilterQueryManagement { long count(); /** - * Retrieves all target filter query which {@link TargetFilterQuery}. - * + * Retrieves all {@link TargetFilterQuery}s which match the given name + * filter. * * @param pageable * pagination parameter * @param name * name filter - * @return the page with the found {@link TargetFilterQuery} + * @return the page with the found {@link TargetFilterQuery}s */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Page findByName(@NotNull Pageable pageable, @NotNull String name); /** - * Retrieves all target filter query which {@link TargetFilterQuery}. - * + * Retrieves all {@link TargetFilterQuery} which match the given RSQL + * filter. * * @param pageable * pagination parameter * @param rsqlFilter * RSQL filter string - * @return the page with the found {@link TargetFilterQuery} + * @return the page with the found {@link TargetFilterQuery}s */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Page findByRsql(@NotNull Pageable pageable, @NotNull String rsqlFilter); /** - * Retrieves all target filter query which have exactly the provided query. + * Retrieves all {@link TargetFilterQuery}s which match the given query. * * @param pageable * pagination parameter * @param query * the query saved in the target filter query - * @return the page with the found {@link TargetFilterQuery} + * @return the page with the found {@link TargetFilterQuery}s */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Page findByQuery(@NotNull Pageable pageable, @NotNull String query); /** - * Retrieves all target filter query which {@link TargetFilterQuery}. - * + * Retrieves all {@link TargetFilterQuery}s which match the given + * auto-assign distribution set and RSQL filter. * * @param pageable * pagination parameter @@ -140,28 +149,26 @@ public interface TargetFilterQueryManagement { * the auto assign distribution set * @param rsqlParam * RSQL filter - * @return the page with the found {@link TargetFilterQuery} + * @return the page with the found {@link TargetFilterQuery}s * * @throws EntityNotFoundException * if DS with given ID does not exist */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) - Page findByAutoAssignDSAndRsql(@NotNull Pageable pageable, long setId, - String rsqlParam); + Page findByAutoAssignDSAndRsql(@NotNull Pageable pageable, long setId, String rsqlParam); /** - * Retrieves all target filter query with auto assign DS which - * {@link TargetFilterQuery}. + * Retrieves all {@link TargetFilterQuery}s with an auto-assign distribution + * set. * - * - * @return the page with the found {@link TargetFilterQuery} + * @return the page with the found {@link TargetFilterQuery}s * @param pageable */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Page findWithAutoAssignDS(@NotNull Pageable pageable); /** - * Find target filter query by id. + * Finds the {@link TargetFilterQuery} by id. * * @param targetFilterQueryId * Target filter query id @@ -172,7 +179,7 @@ public interface TargetFilterQueryManagement { Optional get(long targetFilterQueryId); /** - * Find target filter query by name. + * Finds the {@link TargetFilterQuery} that matches the given name. * * @param targetFilterQueryName * Target filter query name @@ -183,7 +190,7 @@ public interface TargetFilterQueryManagement { Optional getByName(@NotNull String targetFilterQueryName); /** - * updates the {@link TargetFilterQuery}. + * Updates the {@link TargetFilterQuery}. * * @param update * to be updated @@ -193,25 +200,36 @@ public interface TargetFilterQueryManagement { * @throws EntityNotFoundException * if either {@link TargetFilterQuery} and/or autoAssignDs are * provided but not found + * * @throws ConstraintViolationException * if fields are not filled as specified. Check * {@link TargetFilterQueryUpdate} for field constraints. + * + * @throws QuotaExceededException + * if the update contains a new query which addresses too many + * targets (auto-assignments only) */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) TargetFilterQuery update(@NotNull @Valid TargetFilterQueryUpdate update); /** - * updates the {@link TargetFilterQuery#getAutoAssignDistributionSet()}. + * Updates the the auto-assign {@link DistributionSet} of the addressed + * {@link TargetFilterQuery}. * * @param queryId - * to be updated + * of the target filter query to be updated * @param dsId * to be updated or null in order to remove it + * * @return the updated {@link TargetFilterQuery} * * @throws EntityNotFoundException * if either {@link TargetFilterQuery} and/or autoAssignDs are * provided but not found + * + * @throws QuotaExceededException + * if the query that is already associated with this filter + * query addresses too many targets (auto-assignments only) */ @PreAuthorize(SpringEvalExpressions.HAS_AUTH_UPDATE_TARGET) TargetFilterQuery updateAutoAssignDS(long queryId, Long dsId); diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/QuotaExceededException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/QuotaExceededException.java index 80477ca84..6a013836f 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/QuotaExceededException.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/QuotaExceededException.java @@ -17,8 +17,11 @@ import org.eclipse.hawkbit.repository.model.BaseEntity; * */ public final class QuotaExceededException extends AbstractServerRtException { + private static final long serialVersionUID = 1L; + private static final String ASSIGNMENT_QUOTA_EXCEEDED_MESSAGE = "Quota exceeded: Cannot assign %s more %s entities to %s '%s'. The maximum is %s."; + /** * Creates a new QuotaExceededException with * {@link SpServerError#SP_QUOTA_EXCEEDED} error. @@ -28,11 +31,26 @@ public final class QuotaExceededException extends AbstractServerRtException { } /** + * Creates a new QuotaExceededException with a custom error message. + * + * @param message + * The custom error message. + */ + public QuotaExceededException(final String message) { + super(message, SpServerError.SP_QUOTA_EXCEEDED); + } + + /** + * Creates a QuotaExceededException with a custom error message and a root + * cause. + * + * @param message + * The custom error message. * @param cause * for the exception */ - public QuotaExceededException(final Throwable cause) { - super(SpServerError.SP_QUOTA_EXCEEDED, cause); + public QuotaExceededException(final String message, final Throwable cause) { + super(message, SpServerError.SP_QUOTA_EXCEEDED, cause); } /** @@ -57,7 +75,55 @@ public final class QuotaExceededException extends AbstractServerRtException { * that is defined by the repository */ public QuotaExceededException(final String type, final long inserted, final int quota) { - super("Request contains too many entries of {" + type + "}. {" + inserted + "} is bejond the permitted {" + super("Request contains too many entries of {" + type + "}. {" + inserted + "} is beyond the permitted {" + quota + "}.", SpServerError.SP_QUOTA_EXCEEDED); } + + /** + * Creates a QuotaExceededException which is to be thrown when an assignment + * quota is exceeded. + * + * @param type + * The type of the entities that shall be assigned to the + * specified parent entity. + * @param parentType + * The type of the parent entity. + * @param parentId + * The ID of the parent entity. + * @param requested + * The number of entities that shall be assigned to the specified + * parent entity. + * @param quota + * The maximum number of entities that can be assigned to the + * parent entity. + */ + public QuotaExceededException(final Class type, final Class parentType, final Long parentId, + final long requested, final long quota) { + this(type.getSimpleName(), parentType.getSimpleName(), parentId, requested, quota); + } + + /** + * Creates a QuotaExceededException which is to be thrown when an assignment + * quota is exceeded. + * + * @param type + * The type of the entities that shall be assigned to the + * specified parent entity. + * @param parentType + * The type of the parent entity. + * @param parentId + * The ID of the parent entity. + * @param requested + * The number of entities that shall be assigned to the specified + * parent entity. + * @param quota + * The maximum number of entities that can be assigned to the + * parent entity. + */ + public QuotaExceededException(final String type, final String parentType, final Long parentId, final long requested, + final long quota) { + super(String.format(ASSIGNMENT_QUOTA_EXCEEDED_MESSAGE, requested, type, parentType, + parentId != null ? String.valueOf(parentId) : "", quota), SpServerError.SP_QUOTA_EXCEEDED); + } + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupsValidation.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupsValidation.java index 3ef6f1d29..5e5821d1c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupsValidation.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupsValidation.java @@ -21,12 +21,12 @@ public class RolloutGroupsValidation { /** * The total amount of targets in a {@link Rollout} */ - private long totalTargets; + private final long totalTargets; /** * A list containing the count of targets for each {@link RolloutGroup} */ - private List targetsPerGroup; + private final List targetsPerGroup; /** * Instantiates a new validation result diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java index befd00c5d..6e2434a3c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/DurationHelper.java @@ -28,8 +28,8 @@ public final class DurationHelper { * */ public static final class DurationRangeValidator { - final Duration min; - final Duration max; + private final Duration min; + private final Duration max; private DurationRangeValidator(final Duration min, final Duration max) { this.min = min; diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java index 1e0f5e28c..00517dadd 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/PropertiesQuotaManagement.java @@ -48,4 +48,54 @@ public class PropertiesQuotaManagement implements QuotaManagement { return securityProperties.getDos().getMaxMessagesPerActionStatus(); } + @Override + public int getMaxMetaDataEntriesPerSoftwareModule() { + return securityProperties.getDos().getMaxMetaDataEntriesPerSoftwareModule(); + } + + @Override + public int getMaxMetaDataEntriesPerDistributionSet() { + return securityProperties.getDos().getMaxMetaDataEntriesPerDistributionSet(); + } + + @Override + public int getMaxSoftwareModulesPerDistributionSet() { + return securityProperties.getDos().getMaxSoftwareModulesPerDistributionSet(); + } + + @Override + public int getMaxSoftwareModuleTypesPerDistributionSetType() { + return securityProperties.getDos().getMaxSoftwareModuleTypesPerDistributionSetType(); + } + + @Override + public int getMaxArtifactsPerSoftwareModule() { + return securityProperties.getDos().getMaxArtifactsPerSoftwareModule(); + } + + @Override + public int getMaxTargetsPerRolloutGroup() { + return securityProperties.getDos().getMaxTargetsPerRolloutGroup(); + } + + @Override + public int getMaxActionsPerTarget() { + return securityProperties.getDos().getMaxActionsPerTarget(); + } + + @Override + public int getMaxTargetsPerManualAssignment() { + return securityProperties.getDos().getMaxTargetsPerManualAssignment(); + } + + @Override + public int getMaxTargetsPerAutoAssignment() { + return securityProperties.getDos().getMaxTargetsPerAutoAssignment(); + } + + @Override + public long getMaxArtifactSize() { + return securityProperties.getDos().getMaxArtifactSize(); + } + } diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java index 950ec114c..41d72748f 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RolloutHelper.java @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import javax.validation.ValidationException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; @@ -75,9 +76,11 @@ public final class RolloutHelper { */ public static void verifyRolloutGroupParameter(final int amountGroup, final QuotaManagement quotaManagement) { if (amountGroup <= 0) { - throw new ValidationException("the amount of groups cannot be lower than zero"); + throw new ValidationException("The amount of groups cannot be lower than zero"); } else if (amountGroup > quotaManagement.getMaxRolloutGroupsPerRollout()) { - throw new ValidationException("the amount of groups cannot be greater than 500"); + throw new QuotaExceededException( + "The amount of groups cannot be greater than " + quotaManagement.getMaxRolloutGroupsPerRollout()); + } } @@ -89,9 +92,9 @@ public final class RolloutHelper { */ public static void verifyRolloutGroupTargetPercentage(final float percentage) { if (percentage <= 0) { - throw new ValidationException("the percentage must be greater than zero"); + throw new ValidationException("The percentage must be greater than zero"); } else if (percentage > 100) { - throw new ValidationException("the percentage must not be greater than 100"); + throw new ValidationException("The percentage must not be greater than 100"); } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java index e9c82b3d0..bd835f805 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/AbstractDsAssignmentStrategy.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent; @@ -24,6 +25,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -46,17 +48,19 @@ public abstract class AbstractDsAssignmentStrategy { protected final ApplicationContext applicationContext; private final ActionRepository actionRepository; private final ActionStatusRepository actionStatusRepository; + private final QuotaManagement quotaManagement; AbstractDsAssignmentStrategy(final TargetRepository targetRepository, final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher, final ApplicationContext applicationContext, final ActionRepository actionRepository, - final ActionStatusRepository actionStatusRepository) { + final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement) { this.targetRepository = targetRepository; this.afterCommit = afterCommit; this.eventPublisher = eventPublisher; this.applicationContext = applicationContext; this.actionRepository = actionRepository; this.actionStatusRepository = actionStatusRepository; + this.quotaManagement = quotaManagement; } /** @@ -213,6 +217,11 @@ public abstract class AbstractDsAssignmentStrategy { JpaAction createTargetAction(final Map targetsWithActionMap, final JpaTarget target, final JpaDistributionSet set) { + + // enforce the 'max actions per target' quota + assertActionsPerTargetQuota(target, 1); + + // create the action final JpaAction actionForTarget = new JpaAction(); final TargetWithActionType targetWithActionType = targetsWithActionMap.get(target.getControllerId()); actionForTarget.setActionType(targetWithActionType.getActionType()); @@ -237,4 +246,11 @@ public abstract class AbstractDsAssignmentStrategy { return actionStatus; } + + private void assertActionsPerTargetQuota(final Target target, final int requested) { + final int quota = quotaManagement.getMaxActionsPerTarget(); + QuotaHelper.assertAssignmentQuota(target.getId(), requested, quota, Action.class, Target.class, + actionRepository::countByTargetId); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java index 5fda8b9a3..b25d65472 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionStatusRepository.java @@ -39,6 +39,16 @@ public interface ActionStatusRepository */ Long countByAction(JpaAction action); + /** + * Counts {@link ActionStatus} entries of given {@link Action} in + * repository. + * + * @param actionId + * of the action to count status entries for + * @return number of actions in repository + */ + long countByActionId(Long actionId); + /** * Retrieves all {@link ActionStatus} entries from repository of given * ActionId. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetMetadataRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetMetadataRepository.java index dbfc6a11c..ab19ca067 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetMetadataRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetMetadataRepository.java @@ -13,6 +13,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetMetadata; import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; /** @@ -23,4 +24,14 @@ public interface DistributionSetMetadataRepository extends PagingAndSortingRepository, JpaSpecificationExecutor { + /** + * Counts the meta data entries that match the given distribution set ID. + * + * @param id + * of the distribution set. + * + * @return The number of matching meta data entries. + */ + long countByDistributionSetId(@Param("id") Long id); + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTypeRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTypeRepository.java index 5f35625ae..5eb3c8c10 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTypeRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTypeRepository.java @@ -15,6 +15,7 @@ import javax.persistence.EntityManager; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType; import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.TenantAwareBaseEntity; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -40,6 +41,7 @@ public interface DistributionSetTypeRepository * @param isDeleted * to true if only soft deleted entries of * false if undeleted ones + * * @return list of found {@link DistributionSetType}s */ Page findByDeleted(Pageable pageable, boolean isDeleted); @@ -47,7 +49,8 @@ public interface DistributionSetTypeRepository /** * @param isDeleted * to true if only marked as deleted have to be - * count or all undeleted. + * counted or all undeleted. + * * @return number of {@link DistributionSetType}s in the repository. */ long countByDeleted(boolean isDeleted); @@ -59,6 +62,7 @@ public interface DistributionSetTypeRepository * @param softwareModuleType * the software module type to count the distribution set type * which has this software module type assigned + * * @return the number of {@link DistributionSetType}s in the repository * assigned to the given software module type */ @@ -78,8 +82,29 @@ public interface DistributionSetTypeRepository @Query("DELETE FROM JpaDistributionSetType t WHERE t.tenant = :tenant") void deleteByTenant(@Param("tenant") String tenant); + /** + * Retrieves the {@link DistributionSetType}s for the given IDs. Workaround + * for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 + * + * @param ids + * of the types to be located + * + * @return a list of distribution set types + */ @Override - // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477 @Query("SELECT d FROM JpaDistributionSetType d WHERE d.id IN ?1") List findAll(Iterable ids); + + /** + * Counts the {@link SoftwareModuleType}s which are associated with the + * addressed {@link DistributionSetType}. + * + * @param id + * of the distribution set type + * + * @return the number of associated software module types + */ + @Query("SELECT COUNT (e.smType) FROM DistributionSetTypeElement e WHERE e.dsType.id = :id") + long countSmTypesById(@Param("id") Long id); + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaArtifactManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaArtifactManagement.java index b912b4f18..a53d7ebfb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaArtifactManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaArtifactManagement.java @@ -17,15 +17,18 @@ import org.eclipse.hawkbit.artifact.repository.HashNotMatchException; import org.eclipse.hawkbit.artifact.repository.model.AbstractDbArtifact; import org.eclipse.hawkbit.artifact.repository.model.DbArtifactHash; import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.exception.ArtifactDeleteFailedException; import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.InvalidMD5HashException; import org.eclipse.hawkbit.repository.exception.InvalidSHA1HashException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.model.JpaArtifact; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.tenancy.TenantAware; @@ -49,6 +52,8 @@ public class JpaArtifactManagement implements ArtifactManagement { private static final Logger LOG = LoggerFactory.getLogger(JpaArtifactManagement.class); + private static final String MAX_ARTIFACT_SIZE_EXCEEDED = "Quota exceeded: The artifact '%s' (%s bytes) which has been uploaded for software module '%s' exceeds the maximum artifact size of %s bytes."; + private final LocalArtifactRepository localArtifactRepository; private final SoftwareModuleRepository softwareModuleRepository; @@ -57,12 +62,15 @@ public class JpaArtifactManagement implements ArtifactManagement { private final TenantAware tenantAware; + private final QuotaManagement quotaManagement; + JpaArtifactManagement(final LocalArtifactRepository localArtifactRepository, final SoftwareModuleRepository softwareModuleRepository, final ArtifactRepository artifactRepository, - final TenantAware tenantAware) { + final QuotaManagement quotaManagement, final TenantAware tenantAware) { this.localArtifactRepository = localArtifactRepository; this.softwareModuleRepository = softwareModuleRepository; this.artifactRepository = artifactRepository; + this.quotaManagement = quotaManagement; this.tenantAware = tenantAware; } @@ -87,13 +95,16 @@ public class JpaArtifactManagement implements ArtifactManagement { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public Artifact create(final InputStream stream, final long moduleId, final String filename, final String providedMd5Sum, final String providedSha1Sum, final boolean overrideExisting, - final String contentType) { + final String contentType, final long filesize) { AbstractDbArtifact result = null; final SoftwareModule softwareModule = getModuleAndThrowExceptionIfThatFails(moduleId); final Artifact existing = checkForExistingArtifact(filename, overrideExisting, softwareModule); + assertArtifactQuota(moduleId, 1); + assertMaxArtifactSizeQuota(filename, moduleId, filesize); + try { result = artifactRepository.store(tenantAware.getCurrentTenant(), stream, filename, contentType, new DbArtifactHash(providedSha1Sum, providedMd5Sum)); @@ -113,6 +124,23 @@ public class JpaArtifactManagement implements ArtifactManagement { return storeArtifactMetadata(softwareModule, filename, result, existing); } + private void assertArtifactQuota(final long id, final int requested) { + QuotaHelper.assertAssignmentQuota(id, requested, quotaManagement.getMaxArtifactsPerSoftwareModule(), + Artifact.class, SoftwareModule.class, localArtifactRepository::countBySoftwareModuleId); + } + + private void assertMaxArtifactSizeQuota(final String filename, final long id, final long artifactSize) { + final long maxArtifactSize = quotaManagement.getMaxArtifactSize(); + if (maxArtifactSize <= 0) { + return; + } + if (artifactSize > maxArtifactSize) { + final String msg = String.format(MAX_ARTIFACT_SIZE_EXCEEDED, filename, artifactSize, id, maxArtifactSize); + LOG.warn(msg); + throw new QuotaExceededException(msg); + } + } + @Override @Transactional @Retryable(include = { @@ -207,8 +235,8 @@ public class JpaArtifactManagement implements ArtifactManagement { @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public Artifact create(final InputStream inputStream, final long moduleId, final String filename, - final boolean overrideExisting) { - return create(inputStream, moduleId, filename, null, null, overrideExisting, null); + final boolean overrideExisting, final long filesize) { + return create(inputStream, moduleId, filename, null, null, overrideExisting, null, filesize); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index 486807759..5cddb7495 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -44,7 +44,6 @@ import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent; import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent; import org.eclipse.hawkbit.repository.exception.CancelActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; -import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.builder.JpaActionStatusCreate; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; @@ -56,6 +55,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; @@ -535,8 +535,8 @@ public class JpaControllerManagement implements ControllerManagement { break; default: // information status entry - check for a potential DOS attack - checkForTooManyStatusEntries(action); - checkForTooManyStatusMessages(actionStatus); + assertActionStatusQuota(action); + assertActionStatusMessageQuota(actionStatus); break; } @@ -546,12 +546,9 @@ public class JpaControllerManagement implements ControllerManagement { return action; } - private void checkForTooManyStatusMessages(final JpaActionStatus actionStatus) { - if (actionStatus.getMessages().size() > quotaManagement.getMaxMessagesPerActionStatus()) { - throw new QuotaExceededException("ActionStatus messages", actionStatus.getMessages().size(), - quotaManagement.getMaxStatusEntriesPerAction()); - } - + private void assertActionStatusMessageQuota(final JpaActionStatus actionStatus) { + QuotaHelper.assertAssignmentQuota(actionStatus.getId(), actionStatus.getMessages().size(), + quotaManagement.getMaxMessagesPerActionStatus(), "Message", ActionStatus.class.getSimpleName(), null); } private void handleFinishedCancelation(final JpaActionStatus actionStatus, final JpaAction action) { @@ -607,8 +604,8 @@ public class JpaControllerManagement implements ControllerManagement { break; default: // information status entry - check for a potential DOS attack - checkForTooManyStatusEntries(action); - checkForTooManyStatusMessages(actionStatus); + assertActionStatusQuota(action); + assertActionStatusMessageQuota(actionStatus); break; } @@ -628,16 +625,9 @@ public class JpaControllerManagement implements ControllerManagement { targetRepository.save(mergedTarget); } - private void checkForTooManyStatusEntries(final JpaAction action) { - if (quotaManagement.getMaxStatusEntriesPerAction() > 0) { - - final Long statusCount = actionStatusRepository.countByAction(action); - - if (statusCount >= quotaManagement.getMaxStatusEntriesPerAction()) { - throw new QuotaExceededException(ActionStatus.class, statusCount + 1, - quotaManagement.getMaxStatusEntriesPerAction()); - } - } + private void assertActionStatusQuota(final JpaAction action) { + QuotaHelper.assertAssignmentQuota(action.getId(), 1, quotaManagement.getMaxStatusEntriesPerAction(), + ActionStatus.class, Action.class, actionStatusRepository::countByActionId); } private void handleFinishedAndStoreInTargetStatus(final JpaAction action) { @@ -694,16 +684,17 @@ public class JpaControllerManagement implements ControllerManagement { // unknown update mode throw new IllegalStateException("The update mode " + updateMode + " is not supported."); } - - final int attributeCount = controllerAttributes.size(); - if (attributeCount > quotaManagement.getMaxAttributeEntriesPerTarget()) { - throw new QuotaExceededException("Controller attributes", attributeCount, - quotaManagement.getMaxAttributeEntriesPerTarget()); - } + assertTargetAttributesQuota(target); return targetRepository.save(target); } + private void assertTargetAttributesQuota(final JpaTarget target) { + final int limit = quotaManagement.getMaxAttributeEntriesPerTarget(); + QuotaHelper.assertAssignmentQuota(target.getId(), target.getControllerAttributes().size(), limit, "Attribute", + Target.class.getSimpleName(), null); + } + @Override @Transactional @Retryable(include = { @@ -775,8 +766,8 @@ public class JpaControllerManagement implements ControllerManagement { final JpaActionStatus statusMessage = create.build(); statusMessage.setAction(action); - checkForTooManyStatusEntries(action); - checkForTooManyStatusMessages(statusMessage); + assertActionStatusQuota(action); + assertActionStatusMessageQuota(statusMessage); return actionStatusRepository.save(statusMessage); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java index d8b8af3f2..80daba275 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java @@ -28,6 +28,7 @@ import javax.persistence.criteria.Root; import org.eclipse.hawkbit.repository.ActionFields; import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; @@ -46,6 +47,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; @@ -115,6 +117,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { private final OnlineDsAssignmentStrategy onlineDsAssignmentStrategy; private final OfflineDsAssignmentStrategy offlineDsAssignmentStrategy; private final TenantConfigurationManagement tenantConfigurationManagement; + private final QuotaManagement quotaManagement; private final SystemSecurityContext systemSecurityContext; private final Database database; @@ -124,7 +127,7 @@ public class JpaDeploymentManagement implements DeploymentManagement { final AuditorAware auditorProvider, final ApplicationEventPublisher eventPublisher, final ApplicationContext applicationContext, final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, - final TenantConfigurationManagement tenantConfigurationManagement, + final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement, final SystemSecurityContext systemSecurityContext, final Database database) { this.entityManager = entityManager; this.actionRepository = actionRepository; @@ -139,10 +142,11 @@ public class JpaDeploymentManagement implements DeploymentManagement { this.virtualPropertyReplacer = virtualPropertyReplacer; this.txManager = txManager; onlineDsAssignmentStrategy = new OnlineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisher, - applicationContext, actionRepository, actionStatusRepository); + applicationContext, actionRepository, actionStatusRepository, quotaManagement); offlineDsAssignmentStrategy = new OfflineDsAssignmentStrategy(targetRepository, afterCommit, eventPublisher, - applicationContext, actionRepository, actionStatusRepository); + applicationContext, actionRepository, actionStatusRepository, quotaManagement); this.tenantConfigurationManagement = tenantConfigurationManagement; + this.quotaManagement = quotaManagement; this.systemSecurityContext = systemSecurityContext; this.database = database; } @@ -215,8 +219,8 @@ public class JpaDeploymentManagement implements DeploymentManagement { * a list of all targets and their action type * @param actionMessage * an optional message to be written into the action status - * @param offline - * to true in offline case + * @param assignmentStrategy + * the assignment strategy (online /offline) * @return the assignment result * * @throw IncompleteDistributionSetException if mandatory @@ -240,6 +244,11 @@ public class JpaDeploymentManagement implements DeploymentManagement { final List controllerIDs = targetsWithActionType.stream().map(TargetWithActionType::getControllerId) .collect(Collectors.toList()); + // enforce the 'max targets per manual assignment' quota + if (!controllerIDs.isEmpty()) { + assertMaxTargetsPerManualAssignmentQuota(set.getId(), controllerIDs.size()); + } + LOG.debug("assignDistribution({}) to {} targets", set, controllerIDs.size()); final Map targetsWithActionMap = targetsWithActionType.stream() @@ -318,6 +327,20 @@ public class JpaDeploymentManagement implements DeploymentManagement { targetManagement); } + /** + * Enforces the quota defining the maximum number of {@link Target}s per + * manual {@link DistributionSet} assignment. + * + * @param id + * of the distribution set + * @param requested + * number of targets to check + */ + private void assertMaxTargetsPerManualAssignmentQuota(final Long id, final int requested) { + QuotaHelper.assertAssignmentQuota(id, requested, quotaManagement.getMaxTargetsPerManualAssignment(), + Target.class, DistributionSet.class, null); + } + @Override @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(include = { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java index 7a90a2c6d..7eb9f8e8c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java @@ -23,6 +23,7 @@ import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetMetadataFields; import org.eclipse.hawkbit.repository.DistributionSetTagManagement; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.builder.DistributionSetCreate; import org.eclipse.hawkbit.repository.builder.DistributionSetUpdate; @@ -43,6 +44,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.DistributionSetSpecification; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetFilter; @@ -91,6 +93,8 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { private final DistributionSetTypeManagement distributionSetTypeManagement; + private final QuotaManagement quotaManagement; + private final DistributionSetMetadataRepository distributionSetMetadataRepository; private final TargetFilterQueryRepository targetFilterQueryRepository; @@ -118,7 +122,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { JpaDistributionSetManagement(final EntityManager entityManager, final DistributionSetRepository distributionSetRepository, final DistributionSetTagManagement distributionSetTagManagement, final SystemManagement systemManagement, - final DistributionSetTypeManagement distributionSetTypeManagement, + final DistributionSetTypeManagement distributionSetTypeManagement, final QuotaManagement quotaManagement, final DistributionSetMetadataRepository distributionSetMetadataRepository, final TargetFilterQueryRepository targetFilterQueryRepository, final ActionRepository actionRepository, final NoCountPagingRepository criteriaNoCountDao, final ApplicationEventPublisher eventPublisher, @@ -132,6 +136,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { this.distributionSetTagManagement = distributionSetTagManagement; this.systemManagement = systemManagement; this.distributionSetTypeManagement = distributionSetTypeManagement; + this.quotaManagement = quotaManagement; this.distributionSetMetadataRepository = distributionSetMetadataRepository; this.targetFilterQueryRepository = targetFilterQueryRepository; this.actionRepository = actionRepository; @@ -317,6 +322,9 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { assertDistributionSetIsNotAssignedToTargets(setId); final JpaDistributionSet set = findDistributionSetAndThrowExceptionIfNotFound(setId); + + assertSoftwareModuleQuota(setId, modules.size()); + modules.forEach(set::addModule); return distributionSetRepository.save(set); @@ -452,6 +460,8 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { md.forEach(meta -> checkAndThrowAlreadyIfDistributionSetMetadataExists( new DsMetadataCompositeKey(dsId, meta.getKey()))); + assertMetaDataQuota(dsId, md.size()); + final JpaDistributionSet set = touch(dsId); return Collections.unmodifiableList(md.stream() @@ -460,6 +470,18 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { .collect(Collectors.toList())); } + private void assertMetaDataQuota(final Long dsId, final int requested) { + QuotaHelper.assertAssignmentQuota(dsId, requested, quotaManagement.getMaxMetaDataEntriesPerDistributionSet(), + DistributionSetMetadata.class, DistributionSet.class, + distributionSetMetadataRepository::countByDistributionSetId); + } + + private void assertSoftwareModuleQuota(final Long id, final int requested) { + QuotaHelper.assertAssignmentQuota(id, requested, quotaManagement.getMaxSoftwareModulesPerDistributionSet(), + SoftwareModule.class, DistributionSet.class, + softwareModuleRepository::countByAssignedToId); + } + @Override @Transactional @Retryable(include = { @@ -500,9 +522,8 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { private JpaDistributionSet touch(final DistributionSet ds) { // merge base distribution set so optLockRevision gets updated and audit - // log written because - // modifying metadata is modifying the base distribution set itself for - // auditing purposes. + // log written because modifying metadata is modifying the base + // distribution set itself for auditing purposes. final JpaDistributionSet result = entityManager.merge((JpaDistributionSet) ds); result.setLastModifiedAt(0L); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java index 0f53c5b20..668bdf6fe 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java @@ -17,11 +17,13 @@ import java.util.stream.Collectors; import org.eclipse.hawkbit.repository.DistributionSetTypeFields; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.builder.DistributionSetTypeCreate; import org.eclipse.hawkbit.repository.builder.DistributionSetTypeUpdate; import org.eclipse.hawkbit.repository.builder.GenericDistributionSetTypeUpdate; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.builder.JpaDistributionSetTypeCreate; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; @@ -29,6 +31,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.DistributionSetTypeSpecification; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; @@ -64,17 +67,20 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana private final NoCountPagingRepository criteriaNoCountDao; private final Database database; + private final QuotaManagement quotaManagement; + JpaDistributionSetTypeManagement(final DistributionSetTypeRepository distributionSetTypeRepository, final SoftwareModuleTypeRepository softwareModuleTypeRepository, final DistributionSetRepository distributionSetRepository, final VirtualPropertyReplacer virtualPropertyReplacer, final NoCountPagingRepository criteriaNoCountDao, - final Database database) { + final Database database, final QuotaManagement quotaManagement) { this.distributionSetTypeRepository = distributionSetTypeRepository; this.softwareModuleTypeRepository = softwareModuleTypeRepository; this.distributionSetRepository = distributionSetRepository; this.virtualPropertyReplacer = virtualPropertyReplacer; this.criteriaNoCountDao = criteriaNoCountDao; this.database = database; + this.quotaManagement = quotaManagement; } @Override @@ -116,6 +122,7 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana final JpaDistributionSetType type = findDistributionSetTypeAndThrowExceptionIfNotFound(dsTypeId); checkDistributionSetTypeSoftwareModuleTypesIsAllowedToModify(dsTypeId); + assertSoftwareModuleTypeQuota(dsTypeId, softwareModulesTypeIds.size()); modules.forEach(type::addMandatoryModuleType); @@ -138,11 +145,31 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana final JpaDistributionSetType type = findDistributionSetTypeAndThrowExceptionIfNotFound(dsTypeId); checkDistributionSetTypeSoftwareModuleTypesIsAllowedToModify(dsTypeId); + assertSoftwareModuleTypeQuota(dsTypeId, softwareModulesTypeIds.size()); + modules.forEach(type::addOptionalModuleType); return distributionSetTypeRepository.save(type); } + /** + * Enforces the quota specifiying the maximum number of + * {@link SoftwareModuleType}s per {@link DistributionSetType}. + * + * @param id + * of the distribution set type + * @param requested + * number of software module types to check + * + * @throws QuotaExceededException + * if the software module type quota is exceeded + */ + private void assertSoftwareModuleTypeQuota(final long id, final int requested) { + QuotaHelper.assertAssignmentQuota(id, requested, + quotaManagement.getMaxSoftwareModuleTypesPerDistributionSetType(), SoftwareModuleType.class, + DistributionSetType.class, distributionSetTypeRepository::countSmTypesById); + } + @Override @Transactional @Retryable(include = { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index 415b08a40..6cd0921ff 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -52,6 +52,7 @@ import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupConditio import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.RolloutSpecification; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; @@ -226,11 +227,15 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } private Rollout createRolloutGroups(final int amountOfGroups, final RolloutGroupConditions conditions, - final Rollout rollout) { + final JpaRollout rollout) { RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING); RolloutHelper.verifyRolloutGroupConditions(conditions); - final JpaRollout savedRollout = (JpaRollout) rollout; + final JpaRollout savedRollout = rollout; + + // we can enforce the 'max targets per group' quota right here because + // we want to distribute the targets equally to the different groups + assertTargetsPerRolloutGroupQuota(rollout.getTotalTargets() / amountOfGroups); RolloutGroup lastSavedGroup = null; for (int i = 0; i < amountOfGroups; i++) { @@ -269,7 +274,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { RolloutHelper.verifyRolloutInStatus(rollout, RolloutStatus.CREATING); final JpaRollout savedRollout = (JpaRollout) rollout; - // Preparing the groups + // prepare the groups final List groups = groupList.stream() .map(group -> JpaRolloutHelper.prepareRolloutGroupWithDefaultConditions(group, conditions)) .collect(Collectors.toList()); @@ -278,7 +283,13 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { RolloutHelper.verifyRemainingTargets( calculateRemainingTargets(groups, savedRollout.getTargetFilterQuery(), savedRollout.getCreatedAt())); - // Persisting the groups + // check if we need to enforce the 'max targets per group' quota + if (quotaManagement.getMaxTargetsPerRolloutGroup() > 0) { + validateTargetsInGroups(groups, savedRollout.getTargetFilterQuery(), savedRollout.getCreatedAt()) + .getTargetsPerGroup().forEach(this::assertTargetsPerRolloutGroupQuota); + } + + // create and persist the groups (w/o filling them with targets) RolloutGroup lastSavedGroup = null; for (final RolloutGroup srcGroup : groups) { final JpaRolloutGroup group = new JpaRolloutGroup(); @@ -346,7 +357,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { // When all groups are ready the rollout status can be changed to be // ready, too. if (readyGroups == rolloutGroups.size()) { - LOGGER.debug("rollout {} creatin done. Switch to READY.", rollout.getId()); + LOGGER.debug("rollout {} creation done. Switch to READY.", rollout.getId()); rollout.setStatus(RolloutStatus.READY); rollout.setLastCheck(0); rollout.setTotalTargets(totalTargets); @@ -383,9 +394,10 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { return rolloutGroupRepository.save(group); } - long targetsLeftToAdd = expectedInGroup - currentlyInGroup; - try { + + long targetsLeftToAdd = expectedInGroup - currentlyInGroup; + do { // Add up to TRANSACTION_TARGETS of the left targets // In case a TransactionException is thrown this loop aborts @@ -558,6 +570,9 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { final List targetIds = targets.stream().map(Target::getId).collect(Collectors.toList()); actionRepository.switchStatus(Action.Status.CANCELED, targetIds, false, Action.Status.SCHEDULED); targets.forEach(target -> { + + assertActionsPerTargetQuota(target, 1); + final JpaAction action = new JpaAction(); action.setTarget(target); action.setActive(false); @@ -1006,6 +1021,11 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { return rollout; } + @Override + public boolean exists(final long rolloutId) { + return rolloutRepository.exists(rolloutId); + } + private Map> getStatusCountItemForRollout(final List rollouts) { if (rollouts.isEmpty()) { return null; @@ -1044,8 +1064,33 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { } } - @Override - public boolean exists(final long rolloutId) { - return rolloutRepository.exists(rolloutId); + /** + * Enforces the quota defining the maximum number of {@link Target}s per + * {@link RolloutGroup}. + * + * @param group + * The rollout group + * @param requested + * number of targets to check + */ + private void assertTargetsPerRolloutGroupQuota(final long requested) { + final int quota = quotaManagement.getMaxTargetsPerRolloutGroup(); + QuotaHelper.assertAssignmentQuota(requested, quota, Target.class, RolloutGroup.class); } + + /** + * Enforces the quota defining the maximum number of {@link Action}s per + * {@link Target}. + * + * @param target + * The target + * @param requested + * number of actions to check + */ + private void assertActionsPerTargetQuota(final Target target, final int requested) { + final int quota = quotaManagement.getMaxActionsPerTarget(); + QuotaHelper.assertAssignmentQuota(target.getId(), requested, quota, Action.class, Target.class, + actionRepository::countByTargetId); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java index f8c92ef8e..5fb339b18 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java @@ -14,6 +14,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -28,6 +29,7 @@ import javax.persistence.criteria.Root; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.repository.ArtifactManagement; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.SoftwareModuleFields; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; @@ -53,6 +55,7 @@ import org.eclipse.hawkbit.repository.jpa.model.SwMetadataCompositeKey; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.SoftwareModuleSpecification; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.AssignedSoftwareModule; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -103,7 +106,10 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { private final ArtifactManagement artifactManagement; + private final QuotaManagement quotaManagement; + private final VirtualPropertyReplacer virtualPropertyReplacer; + private final Database database; JpaSoftwareModuleManagement(final EntityManager entityManager, @@ -112,8 +118,8 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { final SoftwareModuleMetadataRepository softwareModuleMetadataRepository, final SoftwareModuleTypeRepository softwareModuleTypeRepository, final NoCountPagingRepository criteriaNoCountDao, final AuditorAware auditorProvider, - final ArtifactManagement artifactManagement, final VirtualPropertyReplacer virtualPropertyReplacer, - final Database database) { + final ArtifactManagement artifactManagement, final QuotaManagement quotaManagement, + final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) { this.entityManager = entityManager; this.distributionSetRepository = distributionSetRepository; this.softwareModuleRepository = softwareModuleRepository; @@ -122,6 +128,7 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { this.criteriaNoCountDao = criteriaNoCountDao; this.auditorProvider = auditorProvider; this.artifactManagement = artifactManagement; + this.quotaManagement = quotaManagement; this.virtualPropertyReplacer = virtualPropertyReplacer; this.database = database; } @@ -461,19 +468,13 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public SoftwareModuleMetadata createMetaData(final SoftwareModuleMetadataCreate c) { + final JpaSoftwareModuleMetadataCreate create = (JpaSoftwareModuleMetadataCreate) c; + final Long moduleId = create.getSoftwareModuleId(); + assertSoftwareModuleExists(moduleId); + assertMetaDataQuota(moduleId, 1); - checkAndThrowAlreadyIfSoftwareModuleMetadataExists(create.getSoftwareModuleId(), create); - touch(create.getSoftwareModuleId()); - - return softwareModuleMetadataRepository.save(create.build()); - } - - private void checkAndThrowAlreadyIfSoftwareModuleMetadataExists(final Long moduleId, - final JpaSoftwareModuleMetadataCreate md) { - if (softwareModuleMetadataRepository.exists(new SwMetadataCompositeKey(moduleId, md.getKey()))) { - throwMetadataKeyAlreadyExists(md.getKey()); - } + return saveMetadata(create); } @Override @@ -482,7 +483,71 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) public List createMetaData(final Collection create) { - return create.stream().map(this::createMetaData).collect(Collectors.toList()); + if (!create.isEmpty()) { + + // check if all meta data entries refer to the same software module + final Long moduleId = ((JpaSoftwareModuleMetadataCreate) create.iterator().next()).getSoftwareModuleId(); + if (createJpaMetadataCreateStream(create).allMatch(c -> moduleId.equals(c.getSoftwareModuleId()))) { + + assertSoftwareModuleExists(moduleId); + assertMetaDataQuota(moduleId, create.size()); + + return createJpaMetadataCreateStream(create).map(this::saveMetadata).collect(Collectors.toList()); + + } else { + + // group by software module id to minimize database access + final Map> groups = createJpaMetadataCreateStream(create) + .collect(Collectors.groupingBy(JpaSoftwareModuleMetadataCreate::getSoftwareModuleId)); + return groups.entrySet().stream().flatMap(e -> { + + final Long id = e.getKey(); + final List group = e.getValue(); + + assertSoftwareModuleExists(id); + assertMetaDataQuota(id, group.size()); + + return group.stream().map(this::saveMetadata); + }).collect(Collectors.toList()); + } + } + + return Collections.emptyList(); + } + + private static Stream createJpaMetadataCreateStream( + final Collection create) { + return create.stream().map(c -> (JpaSoftwareModuleMetadataCreate) c); + } + + private SoftwareModuleMetadata saveMetadata(final JpaSoftwareModuleMetadataCreate create) { + assertSoftwareModuleMetadataDoesNotExist(create.getSoftwareModuleId(), create); + return softwareModuleMetadataRepository.save(create.build()); + } + + private void assertSoftwareModuleMetadataDoesNotExist(final Long moduleId, + final JpaSoftwareModuleMetadataCreate md) { + if (softwareModuleMetadataRepository.exists(new SwMetadataCompositeKey(moduleId, md.getKey()))) { + throwMetadataKeyAlreadyExists(md.getKey()); + } + } + + private void assertSoftwareModuleExists(final Long moduleId) { + touch(moduleId); + } + + /** + * Asserts the meta data quota for the software module with the given ID. + * + * @param moduleId + * The software module ID. + * @param requested + * Number of meta data entries to be created. + */ + private void assertMetaDataQuota(final Long moduleId, final int requested) { + final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerSoftwareModule(); + QuotaHelper.assertAssignmentQuota(moduleId, requested, maxMetaData, SoftwareModuleMetadata.class, + SoftwareModule.class, softwareModuleMetadataRepository::countBySoftwareModuleId); } @Override @@ -514,9 +579,8 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { */ private JpaSoftwareModule touch(final SoftwareModule latestModule) { // merge base distribution set so optLockRevision gets updated and audit - // log written because - // modifying metadata is modifying the base distribution set itself for - // auditing purposes. + // log written because modifying metadata is modifying the base + // distribution set itself for auditing purposes. final JpaSoftwareModule result = entityManager.merge((JpaSoftwareModule) latestModule); result.setLastModifiedAt(0L); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java index 580a97494..a82e8c003 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Optional; import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.TargetFields; import org.eclipse.hawkbit.repository.TargetFilterQueryFields; import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; @@ -28,7 +29,9 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTargetFilterQuery; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder; import org.eclipse.hawkbit.repository.jpa.specifications.TargetFilterQuerySpecification; +import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper; import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.springframework.dao.ConcurrencyFailureException; @@ -56,18 +59,24 @@ import com.google.common.collect.Lists; public class JpaTargetFilterQueryManagement implements TargetFilterQueryManagement { private final TargetFilterQueryRepository targetFilterQueryRepository; + private final TargetRepository targetRepository; private final VirtualPropertyReplacer virtualPropertyReplacer; private final DistributionSetManagement distributionSetManagement; + private final QuotaManagement quotaManagement; + private final Database database; JpaTargetFilterQueryManagement(final TargetFilterQueryRepository targetFilterQueryRepository, - final VirtualPropertyReplacer virtualPropertyReplacer, - final DistributionSetManagement distributionSetManagement, final Database database) { + final TargetRepository targetRepository, final VirtualPropertyReplacer virtualPropertyReplacer, + final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement, + final Database database) { this.targetFilterQueryRepository = targetFilterQueryRepository; + this.targetRepository = targetRepository; this.virtualPropertyReplacer = virtualPropertyReplacer; this.distributionSetManagement = distributionSetManagement; + this.quotaManagement = quotaManagement; this.database = database; } @@ -78,6 +87,10 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme public TargetFilterQuery create(final TargetFilterQueryCreate c) { final JpaTargetFilterQueryCreate create = (JpaTargetFilterQueryCreate) c; + // enforce the 'max targets per auto assign' quota right here even if + // the result of the filter query can vary over time + create.getSet().flatMap(set -> create.getQuery()).ifPresent(this::assertMaxTargetsQuota); + return targetFilterQueryRepository.save(create.build()); } @@ -187,7 +200,18 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme final JpaTargetFilterQuery targetFilterQuery = findTargetFilterQueryOrThrowExceptionIfNotFound(update.getId()); update.getName().ifPresent(targetFilterQuery::setName); - update.getQuery().ifPresent(targetFilterQuery::setQuery); + update.getQuery().ifPresent(query -> { + + // enforce the 'max targets per auto assignment'-quota only if the + // query is going to change + if (targetFilterQuery.getAutoAssignDistributionSet() != null + && !query.equals(targetFilterQuery.getQuery())) { + assertMaxTargetsQuota(query); + } + + // set the new query + targetFilterQuery.setQuery(query); + }); return targetFilterQueryRepository.save(targetFilterQuery); } @@ -200,6 +224,13 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme targetFilterQuery.setAutoAssignDistributionSet( Optional.ofNullable(dsId).map(this::findDistributionSetAndThrowExceptionIfNotFound).orElse(null)); + // we cannot be sure that the quota was enforced at creation time + // because the Target Filter Query REST API does not allow to specify an + // auto-assign distribution set when creating a target filter query + if (dsId != null) { + assertMaxTargetsQuota(targetFilterQuery.getQuery()); + } + return targetFilterQueryRepository.save(targetFilterQuery); } @@ -219,4 +250,10 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme return true; } + private void assertMaxTargetsQuota(final String query) { + QuotaHelper.assertAssignmentQuota( + targetRepository.count(RSQLUtility.parse(query, TargetFields.class, virtualPropertyReplacer, database)), + quotaManagement.getMaxTargetsPerAutoAssignment(), Target.class, TargetFilterQuery.class); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/LocalArtifactRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/LocalArtifactRepository.java index 419984128..51d6e5767 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/LocalArtifactRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/LocalArtifactRepository.java @@ -93,12 +93,23 @@ public interface LocalArtifactRepository extends BaseEntityRepository */ - Page findBySoftwareModuleId(Pageable pageReq, final Long swId); + Page findBySoftwareModuleId(Pageable pageReq, Long softwareModuleId); + + /** + * Count the artifacts that are associated with the given software module. + * + * @param softwareModuleId + * software module ID + * + * @return the current number of artifacts associated with the software + * module. + */ + long countBySoftwareModuleId(Long softwareModuleId); /** * Searches for a {@link Artifact} based user provided filename at upload @@ -108,8 +119,9 @@ public interface LocalArtifactRepository extends BaseEntityRepository findFirstByFilenameAndSoftwareModuleId(final String filename, final Long softwareModuleId); + Optional findFirstByFilenameAndSoftwareModuleId(String filename, Long softwareModuleId); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java index 0888c222d..c5eee924e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OfflineDsAssignmentStrategy.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; @@ -43,9 +44,9 @@ public class OfflineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { OfflineDsAssignmentStrategy(final TargetRepository targetRepository, final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher, final ApplicationContext applicationContext, final ActionRepository actionRepository, - final ActionStatusRepository actionStatusRepository) { + final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement) { super(targetRepository, afterCommit, eventPublisher, applicationContext, actionRepository, - actionStatusRepository); + actionStatusRepository, quotaManagement); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java index bddac629b..0c81e94e3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/OnlineDsAssignmentStrategy.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; @@ -41,9 +42,9 @@ public class OnlineDsAssignmentStrategy extends AbstractDsAssignmentStrategy { OnlineDsAssignmentStrategy(final TargetRepository targetRepository, final AfterTransactionCommitExecutor afterCommit, final ApplicationEventPublisher eventPublisher, final ApplicationContext applicationContext, final ActionRepository actionRepository, - final ActionStatusRepository actionStatusRepository) { + final ActionStatusRepository actionStatusRepository, final QuotaManagement quotaManagement) { super(targetRepository, afterCommit, eventPublisher, applicationContext, actionRepository, - actionStatusRepository); + actionStatusRepository, quotaManagement); } @Override diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index 319061d06..afdb944b8 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -23,6 +23,7 @@ import org.eclipse.hawkbit.repository.DistributionSetTagManagement; import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.PropertiesQuotaManagement; +import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryDefaultConfiguration; import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RolloutGroupManagement; @@ -69,6 +70,7 @@ import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.repository.model.helper.SystemManagementHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; @@ -376,7 +378,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { DistributionSetManagement distributionSetManagement(final EntityManager entityManager, final DistributionSetRepository distributionSetRepository, final DistributionSetTagManagement distributionSetTagManagement, final SystemManagement systemManagement, - final DistributionSetTypeManagement distributionSetTypeManagement, + final DistributionSetTypeManagement distributionSetTypeManagement, final QuotaManagement quotaManagement, final DistributionSetMetadataRepository distributionSetMetadataRepository, final TargetFilterQueryRepository targetFilterQueryRepository, final ActionRepository actionRepository, final NoCountPagingRepository criteriaNoCountDao, final ApplicationEventPublisher eventPublisher, @@ -386,7 +388,7 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final DistributionSetTagRepository distributionSetTagRepository, final AfterTransactionCommitExecutor afterCommit, final JpaProperties properties) { return new JpaDistributionSetManagement(entityManager, distributionSetRepository, distributionSetTagManagement, - systemManagement, distributionSetTypeManagement, distributionSetMetadataRepository, + systemManagement, distributionSetTypeManagement, quotaManagement, distributionSetMetadataRepository, targetFilterQueryRepository, actionRepository, criteriaNoCountDao, eventPublisher, applicationContext, tenantAware, virtualPropertyReplacer, softwareModuleRepository, distributionSetTagRepository, afterCommit, properties.getDatabase()); @@ -405,9 +407,10 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final SoftwareModuleTypeRepository softwareModuleTypeRepository, final DistributionSetRepository distributionSetRepository, final VirtualPropertyReplacer virtualPropertyReplacer, final NoCountPagingRepository criteriaNoCountDao, - final JpaProperties properties) { + final JpaProperties properties, final QuotaManagement quotaManagement) { return new JpaDistributionSetTypeManagement(distributionSetTypeRepository, softwareModuleTypeRepository, - distributionSetRepository, virtualPropertyReplacer, criteriaNoCountDao, properties.getDatabase()); + distributionSetRepository, virtualPropertyReplacer, criteriaNoCountDao, properties.getDatabase(), + quotaManagement); } /** @@ -457,22 +460,29 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { * {@link JpaTargetFilterQueryManagement} bean. * * @param targetFilterQueryRepository - * to query entity access + * holding {@link TargetFilterQuery} entities + * @param targetRepository + * holding {@link Target} entities * @param virtualPropertyReplacer * for RSQL handling * @param distributionSetManagement * for auto assign DS access - * + * @param quotaManagement + * to access quotas + * @param properties + * JPA properties + * * @return a new {@link TargetFilterQueryManagement} */ @Bean @ConditionalOnMissingBean TargetFilterQueryManagement targetFilterQueryManagement( - final TargetFilterQueryRepository targetFilterQueryRepository, + final TargetFilterQueryRepository targetFilterQueryRepository, final TargetRepository targetRepository, final VirtualPropertyReplacer virtualPropertyReplacer, - final DistributionSetManagement distributionSetManagement, final JpaProperties properties) { - return new JpaTargetFilterQueryManagement(targetFilterQueryRepository, virtualPropertyReplacer, - distributionSetManagement, properties.getDatabase()); + final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement, + final JpaProperties properties) { + return new JpaTargetFilterQueryManagement(targetFilterQueryRepository, targetRepository, + virtualPropertyReplacer, distributionSetManagement, quotaManagement, properties.getDatabase()); } /** @@ -518,11 +528,11 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final SoftwareModuleMetadataRepository softwareModuleMetadataRepository, final SoftwareModuleTypeRepository softwareModuleTypeRepository, final NoCountPagingRepository criteriaNoCountDao, final AuditorAware auditorProvider, - final ArtifactManagement artifactManagement, final VirtualPropertyReplacer virtualPropertyReplacer, - final JpaProperties properties) { + final ArtifactManagement artifactManagement, final QuotaManagement quotaManagement, + final VirtualPropertyReplacer virtualPropertyReplacer, final JpaProperties properties) { return new JpaSoftwareModuleManagement(entityManager, distributionSetRepository, softwareModuleRepository, softwareModuleMetadataRepository, softwareModuleTypeRepository, criteriaNoCountDao, auditorProvider, - artifactManagement, virtualPropertyReplacer, properties.getDatabase()); + artifactManagement, quotaManagement, virtualPropertyReplacer, properties.getDatabase()); } /** @@ -585,12 +595,12 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final ApplicationEventPublisher eventPublisher, final ApplicationContext applicationContext, final AfterTransactionCommitExecutor afterCommit, final VirtualPropertyReplacer virtualPropertyReplacer, final PlatformTransactionManager txManager, - final TenantConfigurationManagement tenantConfigurationManagement, + final TenantConfigurationManagement tenantConfigurationManagement, final QuotaManagement quotaManagement, final SystemSecurityContext systemSecurityContext, final JpaProperties properties) { return new JpaDeploymentManagement(entityManager, actionRepository, distributionSetRepository, targetRepository, actionStatusRepository, targetManagement, auditorProvider, eventPublisher, applicationContext, - afterCommit, virtualPropertyReplacer, txManager, tenantConfigurationManagement, systemSecurityContext, - properties.getDatabase()); + afterCommit, virtualPropertyReplacer, txManager, tenantConfigurationManagement, quotaManagement, + systemSecurityContext, properties.getDatabase()); } /** @@ -609,9 +619,9 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { @ConditionalOnMissingBean ArtifactManagement artifactManagement(final LocalArtifactRepository localArtifactRepository, final SoftwareModuleRepository softwareModuleRepository, final ArtifactRepository artifactRepository, - final TenantAware tenantAware) { + final QuotaManagement quotaManagement, final TenantAware tenantAware) { return new JpaArtifactManagement(localArtifactRepository, softwareModuleRepository, artifactRepository, - tenantAware); + quotaManagement, tenantAware); } /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleMetadataRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleMetadataRepository.java index 2616d8e03..022023a3b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleMetadataRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleMetadataRepository.java @@ -30,11 +30,49 @@ public interface SoftwareModuleMetadataRepository extends PagingAndSortingRepository, JpaSpecificationExecutor { + /** + * Locates the meta data entries that match the given software module ID and + * target visibility flag. + * + * @param page + * The pagination parameters. + * @param moduleId + * The ID of the software module. + * @param targetVisible + * The target visibility flag. + * + * @return A {@link Page} with the matching meta data entries. + */ Page findBySoftwareModuleIdAndTargetVisible(Pageable page, Long moduleId, boolean targetVisible); + /** + * Locates the meta data entries that match the given software module IDs + * and target visibility flag. + * + * @param page + * The pagination parameters. + * @param moduleId + * List of software module IDs. + * @param targetVisible + * The target visibility flag. + * + * @return A {@link Page} with the matching meta data entries. + */ @Query("SELECT smd.softwareModule.id, smd FROM JpaSoftwareModuleMetadata smd WHERE smd.softwareModule.id IN :moduleId AND smd.targetVisible = :targetVisible") Page findBySoftwareModuleIdInAndTargetVisible(Pageable page, @Param("moduleId") Collection moduleId, @Param("targetVisible") boolean targetVisible); + /** + * Counts the meta data entries that are associated with the addressed + * software module. + * + * @param moduleId + * The ID of the software module. + * + * @return The number of meta data entries associated with the software + * module. + */ + long countBySoftwareModuleId(@Param("moduleId") Long moduleId); + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleRepository.java index 4f4e57768..290e1b107 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleRepository.java @@ -87,6 +87,18 @@ public interface SoftwareModuleRepository */ Page findByAssignedToId(Pageable pageable, Long setId); + /** + * Count the software modules which are assigned to the distribution set + * with the given ID. + * + * @param setId + * the distribution set ID + * + * @return the number of software modules matching the given distribution + * set ID. + */ + long countByAssignedToId(Long setId); + /** * @param pageable * the page request to page the result set diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignScheduler.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignScheduler.java index beeecbd49..bacedf55f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignScheduler.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignScheduler.java @@ -65,9 +65,10 @@ public class AutoAssignScheduler { LOGGER.debug("auto assign schedule checker has been triggered."); // run this code in system code privileged to have the necessary // permission to query and create entities. - systemSecurityContext.runAsSystem(() -> executeAutoAssign()); + systemSecurityContext.runAsSystem(this::executeAutoAssign); } + @SuppressWarnings("squid:S3516") private Object executeAutoAssign() { // workaround eclipselink that is currently not possible to // execute a query without multitenancy if MultiTenant diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java index 829fddbfd..ebef6756b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java @@ -216,7 +216,7 @@ public final class RSQLUtility { private final SimpleTypeConverter simpleTypeConverter; - final Database database; + private final Database database; private JpqQueryRSQLVisitor(final Root root, final CriteriaBuilder cb, final Class enumType, final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java new file mode 100644 index 000000000..f59fc983b --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/utils/QuotaHelper.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2018 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.utils; + +import java.util.function.Function; + +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.validation.annotation.Validated; + +/** + * Helper class to check assignment quotas. + */ +@Validated +public final class QuotaHelper { + + /** + * Class logger + */ + private static final Logger LOG = LoggerFactory.getLogger(QuotaHelper.class); + + private QuotaHelper() { + // no need to instantiate this class + } + + /** + * Asserts the specified assignment quota. + * + * @param requested + * The number of entities that shall be assigned to the parent + * entity. + * @param limit + * The maximum number of entities that may be assigned to the + * parent entity. + * @param type + * The type of the entities that shall be assigned. + * @param parentType + * The type of the parent entity. + * + * @throws QuotaExceededException + * if the assignment operation would cause the quota to be + * exceeded + */ + public static void assertAssignmentQuota(final long requested, final long limit, @NotNull final Class type, + @NotNull final Class parentType) { + assertAssignmentQuota(null, requested, limit, type.getSimpleName(), parentType.getSimpleName(), null); + } + + /** + * Asserts the specified assignment quota. + * + * @param parentId + * The ID of the parent entity. + * @param requested + * The number of entities that shall be assigned to the parent + * entity. + * @param limit + * The maximum number of entities that may be assigned to the + * parent entity. + * @param type + * The type of the entities that shall be assigned. + * @param parentType + * The type of the parent entity. + * @param countFct + * Function to count the entities that are currently assigned to + * the parent entity. + * + * @throws QuotaExceededException + * if the assignment operation would cause the quota to be + * exceeded + */ + public static void assertAssignmentQuota(final Long parentId, final long requested, final long limit, + @NotNull final Class type, @NotNull final Class parentType, final Function countFct) { + assertAssignmentQuota(parentId, requested, limit, type.getSimpleName(), parentType.getSimpleName(), countFct); + } + + /** + * Asserts the specified assignment quota. + * + * @param parentId + * The ID of the parent entity. + * @param requested + * The number of entities that shall be assigned to the parent + * entity. + * @param limit + * The maximum number of entities that may be assigned to the + * parent entity. + * @param type + * The type of the entities that shall be assigned. + * @param parentType + * The type of the parent entity. + * @param countFct + * Function to count the entities that are currently assigned to + * the parent entity. + * + * @throws QuotaExceededException + * if the assignment operation would cause the quota to be + * exceeded + */ + public static void assertAssignmentQuota(final Long parentId, final long requested, final long limit, + @NotNull final String type, @NotNull final String parentType, final Function countFct) { + + // check if the quota is unlimited + if (limit <= 0) { + LOG.debug("Quota 'Max {} entities per {}' is unlimited.", type, parentType); + return; + } + + if (requested > limit) { + final String parentIdStr = parentId != null ? String.valueOf(parentId) : ""; + LOG.warn("Cannot assign {} {} entities to {} '{}' because of the configured quota limit {}.", requested, + type, parentType, parentIdStr, limit); + throw new QuotaExceededException(type, parentType, parentId, requested, limit); + } + + if (parentId != null && countFct != null) { + final long currentCount = countFct.apply(parentId); + if (currentCount + requested > limit) { + LOG.warn( + "Cannot assign {} {} entities to {} '{}' because of the configured quota limit {}. Currently, there are {} {} entities assigned.", + requested, type, parentType, parentId, limit, currentCount, type); + throw new QuotaExceededException(type, parentType, parentId, requested, limit); + } + } + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ArtifactManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ArtifactManagementTest.java index 7dc70fa9a..112f7388f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ArtifactManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ArtifactManagementTest.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.repository.jpa; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -17,6 +18,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.security.NoSuchAlgorithmException; +import java.util.List; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; @@ -25,6 +27,7 @@ import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.event.remote.SoftwareModuleDeletedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.model.JpaArtifact; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule; import org.eclipse.hawkbit.repository.model.Artifact; @@ -35,6 +38,8 @@ import org.eclipse.hawkbit.repository.test.util.HashGeneratorUtils; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.junit.Test; +import com.google.common.collect.Lists; + import ru.yandex.qatools.allure.annotations.Description; import ru.yandex.qatools.allure.annotations.Features; import ru.yandex.qatools.allure.annotations.Stories; @@ -67,12 +72,13 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { @ExpectEvents({ @Expect(type = SoftwareModuleDeletedEvent.class, count = 0) }) public void entityQueriesReferringToNotExistingEntitiesThrowsException() throws URISyntaxException { - verifyThrownExceptionBy(() -> artifactManagement.create(IOUtils.toInputStream("test", "UTF-8"), NOT_EXIST_IDL, - "xxx", null, null, false, null), "SoftwareModule"); + final String artifactData = "test"; + final int artifactSize = artifactData.length(); + verifyThrownExceptionBy(() -> artifactManagement.create(IOUtils.toInputStream(artifactData, "UTF-8"), + NOT_EXIST_IDL, "xxx", null, null, false, null, artifactSize), "SoftwareModule"); - verifyThrownExceptionBy( - () -> artifactManagement.create(IOUtils.toInputStream("test", "UTF-8"), 1234L, "xxx", false), - "SoftwareModule"); + verifyThrownExceptionBy(() -> artifactManagement.create(IOUtils.toInputStream(artifactData, "UTF-8"), 1234L, + "xxx", false, artifactSize), "SoftwareModule"); verifyThrownExceptionBy(() -> artifactManagement.delete(NOT_EXIST_IDL), "Artifact"); @@ -86,59 +92,128 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Test if a local artifact can be created by API including metadata.") public void createArtifact() throws NoSuchAlgorithmException, IOException { - // checkbaseline + + // check baseline assertThat(softwareModuleRepository.findAll()).hasSize(0); assertThat(artifactRepository.findAll()).hasSize(0); - JpaSoftwareModule sm = new JpaSoftwareModule(osType, "name 1", "version 1", null, null); - sm = softwareModuleRepository.save(sm); + final JpaSoftwareModule sm = softwareModuleRepository + .save(new JpaSoftwareModule(osType, "name 1", "version 1", null, null)); + final JpaSoftwareModule sm2 = softwareModuleRepository + .save(new JpaSoftwareModule(osType, "name 2", "version 2", null, null)); + softwareModuleRepository.save(new JpaSoftwareModule(osType, "name 3", "version 3", null, null)); - JpaSoftwareModule sm2 = new JpaSoftwareModule(osType, "name 2", "version 2", null, null); - sm2 = softwareModuleRepository.save(sm2); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); - JpaSoftwareModule sm3 = new JpaSoftwareModule(osType, "name 3", "version 3", null, null); - sm3 = softwareModuleRepository.save(sm3); + try (final InputStream inputStream1 = new ByteArrayInputStream(random); + final InputStream inputStream2 = new ByteArrayInputStream(random); + final InputStream inputStream3 = new ByteArrayInputStream(random); + final InputStream inputStream4 = new ByteArrayInputStream(random);) { - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final Artifact result = artifactManagement.create(inputStream1, sm.getId(), "file1", false, artifactSize); + artifactManagement.create(inputStream2, sm.getId(), "file11", false, artifactSize); + artifactManagement.create(inputStream3, sm.getId(), "file12", false, artifactSize); + final Artifact result2 = artifactManagement.create(inputStream4, sm2.getId(), "file2", false, artifactSize); - final Artifact result = artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", false); - artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file11", false); - artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file12", false); - final Artifact result2 = artifactManagement.create(new ByteArrayInputStream(random), sm2.getId(), "file2", - false); + assertThat(result).isInstanceOf(Artifact.class); + assertThat(result.getSoftwareModule().getId()).isEqualTo(sm.getId()); + assertThat(result2.getSoftwareModule().getId()).isEqualTo(sm2.getId()); + assertThat(((JpaArtifact) result).getFilename()).isEqualTo("file1"); + assertThat(((JpaArtifact) result).getSha1Hash()).isNotNull(); + assertThat(result).isNotEqualTo(result2); + assertThat(((JpaArtifact) result).getSha1Hash()).isEqualTo(((JpaArtifact) result2).getSha1Hash()); - assertThat(result).isInstanceOf(Artifact.class); - assertThat(result.getSoftwareModule().getId()).isEqualTo(sm.getId()); - assertThat(result2.getSoftwareModule().getId()).isEqualTo(sm2.getId()); - assertThat(((JpaArtifact) result).getFilename()).isEqualTo("file1"); - assertThat(((JpaArtifact) result).getSha1Hash()).isNotNull(); - assertThat(result).isNotEqualTo(result2); - assertThat(((JpaArtifact) result).getSha1Hash()).isEqualTo(((JpaArtifact) result2).getSha1Hash()); + assertThat(artifactManagement.getByFilename("file1").get().getSha1Hash()) + .isEqualTo(HashGeneratorUtils.generateSHA1(random)); + assertThat(artifactManagement.getByFilename("file1").get().getMd5Hash()) + .isEqualTo(HashGeneratorUtils.generateMD5(random)); - assertThat(artifactManagement.getByFilename("file1").get().getSha1Hash()) - .isEqualTo(HashGeneratorUtils.generateSHA1(random)); - assertThat(artifactManagement.getByFilename("file1").get().getMd5Hash()) - .isEqualTo(HashGeneratorUtils.generateMD5(random)); + assertThat(artifactRepository.findAll()).hasSize(4); + assertThat(softwareModuleRepository.findAll()).hasSize(3); - assertThat(artifactRepository.findAll()).hasSize(4); - assertThat(softwareModuleRepository.findAll()).hasSize(3); + assertThat(softwareModuleManagement.get(sm.getId()).get().getArtifacts()).hasSize(3); + } - assertThat(softwareModuleManagement.get(sm.getId()).get().getArtifacts()).hasSize(3); + } + + @Test + @Description("Verifies that the quota specifying the maximum number of artifacts per software module is enforced.") + public void createArtifactsUntilQuotaIsExceeded() throws NoSuchAlgorithmException, IOException { + + // create a software module + final JpaSoftwareModule sm1 = softwareModuleRepository + .save(new JpaSoftwareModule(osType, "sm1", "1.0", null, null)); + + // now create artifacts for this module until the quota is exceeded + final long maxArtifacts = quotaManagement.getMaxArtifactsPerSoftwareModule(); + final List artifactIds = Lists.newArrayList(); + final int artifactSize = 5 * 1024; + for (int i = 0; i < maxArtifacts; ++i) { + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); + try (final InputStream inputStream = new ByteArrayInputStream(random)) { + artifactIds.add( + artifactManagement.create(inputStream, sm1.getId(), "file" + i, false, artifactSize).getId()); + } + } + assertThat(artifactRepository.findBySoftwareModuleId(PAGE, sm1.getId()).getTotalElements()) + .isEqualTo(maxArtifacts); + + // create one mode to trigger the quota exceeded error + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> { + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); + try (final InputStream inputStream = new ByteArrayInputStream(random)) { + artifactManagement.create(inputStream, sm1.getId(), "file" + maxArtifacts, false, artifactSize); + } + }); + + // delete one of the artifacts + artifactManagement.delete(artifactIds.get(0)); + assertThat(artifactRepository.findBySoftwareModuleId(PAGE, sm1.getId()).getTotalElements()) + .isEqualTo(maxArtifacts - 1); + + // now we should be able to create an artifact again + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); + try (final InputStream inputStream = new ByteArrayInputStream(random)) { + artifactManagement.create(inputStream, sm1.getId(), "fileXYZ", false, artifactSize); + assertThat(artifactRepository.findBySoftwareModuleId(PAGE, sm1.getId()).getTotalElements()) + .isEqualTo(maxArtifacts); + } + } + + @Test + @Description("Verifies that you cannot create artifacts which exceed the configured maximum size.") + public void createArtifactFailsIfTooLarge() throws NoSuchAlgorithmException, IOException { + + // create a software module + final JpaSoftwareModule sm1 = softwareModuleRepository + .save(new JpaSoftwareModule(osType, "sm1", "1.0", null, null)); + + // create an artifact that exceeds the configured quota + final long maxSize = quotaManagement.getMaxArtifactSize(); + final int artifactSize = Math.toIntExact(maxSize) + 8; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); + try (final InputStream inputStream = new ByteArrayInputStream(random)) { + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> artifactManagement.create(inputStream, sm1.getId(), "file", false, artifactSize)); + } } @Test @Description("Tests hard delete directly on repository.") public void hardDeleteSoftwareModule() throws NoSuchAlgorithmException, IOException { - JpaSoftwareModule sm = new JpaSoftwareModule(osType, "name 1", "version 1", null, null); - sm = softwareModuleRepository.save(sm); - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final JpaSoftwareModule sm = softwareModuleRepository + .save(new JpaSoftwareModule(osType, "name 1", "version 1", null, null)); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); + try (final InputStream inputStream = new ByteArrayInputStream(random)) { + artifactManagement.create(inputStream, sm.getId(), "file1", false, artifactSize); + assertThat(artifactRepository.findAll()).hasSize(1); - artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", false); - assertThat(artifactRepository.findAll()).hasSize(1); - - softwareModuleRepository.deleteAll(); - assertThat(artifactRepository.findAll()).hasSize(0); + softwareModuleRepository.deleteAll(); + assertThat(artifactRepository.findAll()).hasSize(0); + } } /** @@ -152,97 +227,109 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Tests the deletion of a local artifact including metadata.") public void deleteArtifact() throws NoSuchAlgorithmException, IOException { - JpaSoftwareModule sm = new JpaSoftwareModule(osType, "name 1", "version 1", null, null); - sm = softwareModuleRepository.save(sm); - JpaSoftwareModule sm2 = new JpaSoftwareModule(osType, "name 2", "version 2", null, null); - sm2 = softwareModuleRepository.save(sm2); + final JpaSoftwareModule sm = softwareModuleRepository + .save(new JpaSoftwareModule(osType, "name 1", "version 1", null, null)); + final JpaSoftwareModule sm2 = softwareModuleRepository + .save(new JpaSoftwareModule(osType, "name 2", "version 2", null, null)); assertThat(artifactRepository.findAll()).isEmpty(); - final Artifact result = artifactManagement.create(new RandomGeneratedInputStream(5 * 1024), sm.getId(), "file1", - false); - final Artifact result2 = artifactManagement.create(new RandomGeneratedInputStream(5 * 1024), sm2.getId(), - "file2", false); + final int artifactSize = 5 * 1024; + try (final InputStream inputStream1 = new RandomGeneratedInputStream(artifactSize); + final InputStream inputStream2 = new RandomGeneratedInputStream(artifactSize)) { - assertThat(artifactRepository.findAll()).hasSize(2); + final Artifact result = artifactManagement.create(inputStream1, sm.getId(), "file1", false, artifactSize); + final Artifact result2 = artifactManagement.create(inputStream2, sm2.getId(), "file2", false, artifactSize); - assertThat(result.getId()).isNotNull(); - assertThat(result2.getId()).isNotNull(); - assertThat(((JpaArtifact) result).getSha1Hash()).isNotEqualTo(((JpaArtifact) result2).getSha1Hash()); + assertThat(artifactRepository.findAll()).hasSize(2); - assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) - .isNotNull(); - assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash())) - .isNotNull(); + assertThat(result.getId()).isNotNull(); + assertThat(result2.getId()).isNotNull(); + assertThat(((JpaArtifact) result).getSha1Hash()).isNotEqualTo(((JpaArtifact) result2).getSha1Hash()); - artifactManagement.delete(result.getId()); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNotNull(); + assertThat( + binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash())) + .isNotNull(); - assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) - .isNull(); - assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash())) - .isNotNull(); + artifactManagement.delete(result.getId()); - artifactManagement.delete(result2.getId()); - assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash())) - .isNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNull(); + assertThat( + binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash())) + .isNotNull(); - assertThat(artifactRepository.findAll()).hasSize(0); + artifactManagement.delete(result2.getId()); + assertThat( + binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result2.getSha1Hash())) + .isNull(); + + assertThat(artifactRepository.findAll()).hasSize(0); + } } @Test @Description("Test the deletion of an artifact metadata where the binary is still linked to another " + "metadata element. The expected result is that the metadata is deleted but the binary kept.") public void deleteDuplicateArtifacts() throws NoSuchAlgorithmException, IOException { - JpaSoftwareModule sm = new JpaSoftwareModule(osType, "name 1", "version 1", null, null); - sm = softwareModuleRepository.save(sm); - JpaSoftwareModule sm2 = new JpaSoftwareModule(osType, "name 2", "version 2", null, null); - sm2 = softwareModuleRepository.save(sm2); + final JpaSoftwareModule sm = softwareModuleRepository + .save(new JpaSoftwareModule(osType, "name 1", "version 1", null, null)); + final JpaSoftwareModule sm2 = softwareModuleRepository + .save(new JpaSoftwareModule(osType, "name 2", "version 2", null, null)); - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); - final Artifact result = artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", false); - final Artifact result2 = artifactManagement.create(new ByteArrayInputStream(random), sm2.getId(), "file2", - false); + try (final InputStream inputStream1 = new ByteArrayInputStream(random); + final InputStream inputStream2 = new ByteArrayInputStream(random)) { + final Artifact result = artifactManagement.create(inputStream1, sm.getId(), "file1", false, artifactSize); + final Artifact result2 = artifactManagement.create(inputStream2, sm2.getId(), "file2", false, artifactSize); - assertThat(artifactRepository.findAll()).hasSize(2); - assertThat(result.getId()).isNotNull(); - assertThat(result2.getId()).isNotNull(); - assertThat(((JpaArtifact) result).getSha1Hash()).isEqualTo(((JpaArtifact) result2).getSha1Hash()); + assertThat(artifactRepository.findAll()).hasSize(2); + assertThat(result.getId()).isNotNull(); + assertThat(result2.getId()).isNotNull(); + assertThat(((JpaArtifact) result).getSha1Hash()).isEqualTo(((JpaArtifact) result2).getSha1Hash()); - assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) - .isNotNull(); - artifactManagement.delete(result.getId()); - assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) - .isNotNull(); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNotNull(); + artifactManagement.delete(result.getId()); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNotNull(); - artifactManagement.delete(result2.getId()); - assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) - .isNull(); + artifactManagement.delete(result2.getId()); + assertThat(binaryArtifactRepository.getArtifactBySha1(tenantAware.getCurrentTenant(), result.getSha1Hash())) + .isNull(); + } } @Test @Description("Loads an local artifact based on given ID.") public void findArtifact() throws NoSuchAlgorithmException, IOException { - final Artifact result = artifactManagement.create(new RandomGeneratedInputStream(5 * 1024), - testdataFactory.createSoftwareModuleOs().getId(), "file1", false); - - assertThat(artifactManagement.get(result.getId()).get()).isEqualTo(result); + final int artifactSize = 5 * 1024; + try (final InputStream inputStream = new RandomGeneratedInputStream(artifactSize)) { + final Artifact result = artifactManagement.create(inputStream, + testdataFactory.createSoftwareModuleOs().getId(), "file1", false, artifactSize); + assertThat(artifactManagement.get(result.getId()).get()).isEqualTo(result); + } } @Test @Description("Loads an artifact binary based on given ID.") public void loadStreamOfArtifact() throws NoSuchAlgorithmException, IOException { - final byte random[] = RandomStringUtils.random(5 * 1024).getBytes(); - - final Artifact result = artifactManagement.create(new ByteArrayInputStream(random), - testdataFactory.createSoftwareModuleOs().getId(), "file1", false); - - try (InputStream fileInputStream = artifactManagement.loadArtifactBinary(result.getSha1Hash()).get() - .getFileInputStream()) { - assertTrue("The stored binary matches the given binary", - IOUtils.contentEquals(new ByteArrayInputStream(random), fileInputStream)); + final int artifactSize = 5 * 1024; + final byte random[] = RandomStringUtils.random(artifactSize).getBytes(); + try (final InputStream input = new ByteArrayInputStream(random)) { + final Artifact result = artifactManagement.create(input, testdataFactory.createSoftwareModuleOs().getId(), + "file1", false, artifactSize); + try (final InputStream inputStream = artifactManagement.loadArtifactBinary(result.getSha1Hash()).get() + .getFileInputStream()) { + assertTrue("The stored binary matches the given binary", + IOUtils.contentEquals(new ByteArrayInputStream(random), inputStream)); + } } } @@ -260,27 +347,31 @@ public class ArtifactManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Searches an artifact through the relations of a software module.") - public void findArtifactBySoftwareModule() { + public void findArtifactBySoftwareModule() throws IOException { final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); - assertThat(artifactManagement.findBySoftwareModule(PAGE, sm.getId())).isEmpty(); - artifactManagement.create(new RandomGeneratedInputStream(5 * 1024), sm.getId(), "file1", false); - - assertThat(artifactManagement.findBySoftwareModule(PAGE, sm.getId())).hasSize(1); + final int artifactSize = 5 * 1024; + try (final InputStream input = new RandomGeneratedInputStream(artifactSize)) { + artifactManagement.create(input, sm.getId(), "file1", false, artifactSize); + assertThat(artifactManagement.findBySoftwareModule(PAGE, sm.getId())).hasSize(1); + } } @Test @Description("Searches an artifact through the relations of a software module and the filename.") - public void findByFilenameAndSoftwareModule() { + public void findByFilenameAndSoftwareModule() throws IOException { final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); assertThat(artifactManagement.getByFilenameAndSoftwareModule("file1", sm.getId())).isNotPresent(); - artifactManagement.create(new RandomGeneratedInputStream(5 * 1024), sm.getId(), "file1", false); - artifactManagement.create(new RandomGeneratedInputStream(5 * 1024), sm.getId(), "file2", false); - - assertThat(artifactManagement.getByFilenameAndSoftwareModule("file1", sm.getId())).isPresent(); + final int artifactSize = 5 * 1024; + try (final InputStream inputStream1 = new RandomGeneratedInputStream(artifactSize); + final InputStream inputStream2 = new RandomGeneratedInputStream(artifactSize)) { + artifactManagement.create(inputStream1, sm.getId(), "file1", false, artifactSize); + artifactManagement.create(inputStream2, sm.getId(), "file2", false, artifactSize); + assertThat(artifactManagement.getByFilenameAndSoftwareModule("file1", sm.getId())).isPresent(); + } } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index 5c50089fb..7697b26f0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.validation.ConstraintViolationException; @@ -430,7 +431,8 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = SoftwareModuleCreatedEvent.class, count = 6), @Expect(type = SoftwareModuleUpdatedEvent.class, count = 2) }) public void hasTargetArtifactAssignedIsTrueWithMultipleArtifacts() { - final byte[] random = RandomUtils.nextBytes(5 * 1024); + final int artifactSize = 5 * 1024; + final byte[] random = RandomUtils.nextBytes(artifactSize); final DistributionSet ds = testdataFactory.createDistributionSet(""); final DistributionSet ds2 = testdataFactory.createDistributionSet("2"); @@ -438,9 +440,9 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { // create two artifacts with identical SHA1 hash final Artifact artifact = artifactManagement.create(new ByteArrayInputStream(random), - ds.findFirstModuleByType(osType).get().getId(), "file1", false); + ds.findFirstModuleByType(osType).get().getId(), "file1", false, artifactSize); final Artifact artifact2 = artifactManagement.create(new ByteArrayInputStream(random), - ds2.findFirstModuleByType(osType).get().getId(), "file1", false); + ds2.findFirstModuleByType(osType).get().getId(), "file1", false, artifactSize); assertThat(artifact.getSha1Hash()).isEqualTo(artifact2.getSha1Hash()); assertThat( @@ -793,7 +795,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { @Expect(type = TargetUpdatedEvent.class, count = 2) }) public void updateTargetAttributesFailsIfTooManyEntries() throws Exception { final String controllerId = "test123"; - final int allowedAttributes = 10; + final int allowedAttributes = quotaManagement.getMaxAttributeEntriesPerTarget(); testdataFactory.createTarget(controllerId); assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> securityRule @@ -880,4 +882,69 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { assertThat(messages.get(0)).as("Message of action-status").isEqualTo("proceeding message 2"); assertThat(messages.get(1)).as("Message of action-status").isEqualTo("proceeding message 1"); } + + @Test + @Description("Verifies that the quota specifying the maximum number of status entries per action is enforced.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 2), + @Expect(type = DistributionSetCreatedEvent.class, count = 2), + @Expect(type = ActionCreatedEvent.class, count = 2), @Expect(type = TargetUpdatedEvent.class, count = 2), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 2), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 6) }) + public void addActionStatusUpdatesUntilQuotaIsExceeded() { + + // any distribution set assignment causes 1 status entity to be created + final int maxStatusEntries = quotaManagement.getMaxStatusEntriesPerAction() - 1; + + // test for informational status + final Long actionId1 = assignDistributionSet(testdataFactory.createDistributionSet("ds1"), + testdataFactory.createTargets(1, "t1")).getActions().get(0); + assertThat(actionId1).isNotNull(); + for (int i = 0; i < maxStatusEntries; ++i) { + controllerManagement.addInformationalActionStatus(entityFactory.actionStatus().create(actionId1) + .status(Status.WARNING).message("Msg " + i).occurredAt(System.currentTimeMillis())); + } + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> controllerManagement + .addInformationalActionStatus(entityFactory.actionStatus().create(actionId1).status(Status.WARNING))); + + // test for update status (and mixed case) + final Long actionId2 = assignDistributionSet(testdataFactory.createDistributionSet("ds2"), + testdataFactory.createTargets(1, "t2")).getActions().get(0); + assertThat(actionId2).isNotEqualTo(actionId1); + for (int i = 0; i < maxStatusEntries; ++i) { + controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(actionId2) + .status(Status.WARNING).message("Msg " + i).occurredAt(System.currentTimeMillis())); + } + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> controllerManagement + .addInformationalActionStatus(entityFactory.actionStatus().create(actionId2).status(Status.WARNING))); + + } + + @Test + @Description("Verifies that the quota specifying the maximum number of messages per action status is enforced.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = DistributionSetCreatedEvent.class, count = 1), + @Expect(type = ActionCreatedEvent.class, count = 1), @Expect(type = TargetUpdatedEvent.class, count = 1), + @Expect(type = TargetAssignDistributionSetEvent.class, count = 1), + @Expect(type = SoftwareModuleCreatedEvent.class, count = 3) }) + public void createActionStatusWithTooManyMessages() { + + final int maxMessages = quotaManagement.getMaxMessagesPerActionStatus(); + + final Long actionId = assignDistributionSet(testdataFactory.createDistributionSet("ds1"), + testdataFactory.createTargets(1)).getActions().get(0); + assertThat(actionId).isNotNull(); + + final List messages = Lists.newArrayList(); + IntStream.range(0, maxMessages).forEach(i -> messages.add(i, "msg")); + + assertThat(controllerManagement.addInformationalActionStatus( + entityFactory.actionStatus().create(actionId).messages(messages).status(Status.WARNING))).isNotNull(); + + messages.add("msg"); + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> controllerManagement.addInformationalActionStatus( + entityFactory.actionStatus().create(actionId).messages(messages).status(Status.WARNING))); + + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java index 0998424f5..f2a3c6de6 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DeploymentManagementTest.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.eclipse.hawkbit.repository.ActionStatusFields; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; @@ -35,6 +36,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.ForceQuitActionNotAllowedException; import org.eclipse.hawkbit.repository.exception.IncompleteDistributionSetException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaActionStatus; @@ -171,6 +173,37 @@ public class DeploymentManagementTest extends AbstractJpaIntegrationTest { assertThat(actions.getContent().get(0).getId()).as("Action of target").isEqualTo(actionId); } + @Test + @Description("Test verifies that the 'max actions per target' quota is enforced if the assigned distribution set is changed permanently.") + public void changeDistributionSetAssignmentUntilMaxActionsPerTargetQuotaIsExceeded() { + + final int maxActions = quotaManagement.getMaxActionsPerTarget(); + final List testTargets = testdataFactory.createTargets(1); + final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); + final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); + final DistributionSet ds3 = testdataFactory.createDistributionSet("ds3"); + + IntStream.range(0, maxActions).forEach(i -> { + assignDistributionSet(i % 2 == 0 ? ds1 : ds2, testTargets); + }); + + // change the distribution set one last time to trigger a quota hit + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> assignDistributionSet(ds3, testTargets)); + } + + @Test + @Description("Assigns the same distribution set to many targets until the 'max targets per manual assignment' quota is exceeded.") + public void assignDistributionSetUntilQuotaIsExceeded() { + + final int maxTargets = quotaManagement.getMaxTargetsPerManualAssignment(); + final DistributionSet ds = testdataFactory.createDistributionSet(); + + assignDistributionSet(ds, testdataFactory.createTargets(maxTargets, "ok")); + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> assignDistributionSet(ds, testdataFactory.createTargets(maxTargets + 1, "fail"))); + } + @Test @Description("Test verifies that action-states of an action are found by using id-based search.") public void findActionStatusByActionId() { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java index d5ad050d4..8d316d45b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetManagementTest.java @@ -32,6 +32,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedE import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.UnsupportedSoftwareModuleForThisDistributionSetException; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetMetadata; @@ -42,6 +43,7 @@ import org.eclipse.hawkbit.repository.model.DistributionSetFilter.DistributionSe import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.DistributionSetType; +import org.eclipse.hawkbit.repository.model.MetaData; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.test.matcher.Expect; @@ -333,6 +335,51 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { assertThat(createdMetadata.getValue()).isEqualTo(knownValue); } + @Test + @Description("Verifies the enforcement of the metadata quota per distribution set.") + public void createDistributionSetMetadataUntilQuotaIsExceeded() { + + // add meta data one by one + final DistributionSet ds1 = testdataFactory.createDistributionSet("ds1"); + final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerDistributionSet(); + for (int i = 0; i < maxMetaData; ++i) { + assertThat((JpaDistributionSetMetadata) createDistributionSetMetadata(ds1.getId(), + new JpaDistributionSetMetadata("k" + i, ds1, "v" + i))).isNotNull(); + } + + // quota exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> createDistributionSetMetadata(ds1.getId(), createDistributionSetMetadata(ds1.getId(), + new JpaDistributionSetMetadata("k" + maxMetaData, ds1, "v" + maxMetaData)))); + + // add multiple meta data entries at once + final DistributionSet ds2 = testdataFactory.createDistributionSet("ds2"); + final List metaData2 = new ArrayList<>(); + for (int i = 0; i < maxMetaData + 1; ++i) { + metaData2.add(new JpaDistributionSetMetadata("k" + i, ds2, "v" + i)); + } + // verify quota is exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> createDistributionSetMetadata(ds2.getId(), metaData2)); + + // add some meta data entries + final DistributionSet ds3 = testdataFactory.createDistributionSet("ds3"); + final int firstHalf = Math.round(maxMetaData / 2); + for (int i = 0; i < firstHalf; ++i) { + createDistributionSetMetadata(ds3.getId(), new JpaDistributionSetMetadata("k" + i, ds3, "v" + i)); + } + // add too many data entries + final int secondHalf = maxMetaData - firstHalf; + final List metaData3 = new ArrayList<>(); + for (int i = 0; i < secondHalf + 1; ++i) { + metaData3.add(new JpaDistributionSetMetadata("kk" + i, ds3, "vv" + i)); + } + // verify quota is exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> createDistributionSetMetadata(ds3.getId(), metaData3)); + + } + @Test @Description("Ensures that distribution sets can assigned and unassigned to a distribution set tag.") public void assignAndUnassignDistributionSetToTag() { @@ -400,8 +447,8 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { @Description("Ensures that it is not possible to add a software module that is not defined of the DS's type.") public void updateDistributionSetUnsupportedModuleFails() { final DistributionSet set = distributionSetManagement - .create(entityFactory.distributionSet().create().name("agent-hub2") - .version( + .create(entityFactory + .distributionSet().create().name("agent-hub2").version( "1.0.5") .type(distributionSetTypeManagement.create(entityFactory.distributionSetType().create() .key("test").name("test").mandatory(Arrays.asList(osType.getId()))).getKey())); @@ -443,6 +490,46 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { assertThat(ds.isRequiredMigrationStep()).isTrue(); } + @Test + @Description("Verifies the enforcement of the software module quota per distribution set.") + public void assignSoftwareModulesUntilQuotaIsExceeded() { + + // create some software modules + final int maxModules = quotaManagement.getMaxSoftwareModulesPerDistributionSet(); + final List modules = Lists.newArrayList(); + for (int i = 0; i < maxModules + 1; ++i) { + modules.add(testdataFactory.createSoftwareModuleApp("sm" + i).getId()); + } + + // assign software modules one by one + final DistributionSet ds1 = testdataFactory.createDistributionSetWithNoSoftwareModules("ds1", "1.0"); + for (int i = 0; i < maxModules; ++i) { + distributionSetManagement.assignSoftwareModules(ds1.getId(), Collections.singletonList(modules.get(i))); + } + // add one more to cause the quota to be exceeded + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> { + distributionSetManagement.assignSoftwareModules(ds1.getId(), + Collections.singletonList(modules.get(maxModules))); + }); + + // assign all software modules at once + final DistributionSet ds2 = testdataFactory.createDistributionSetWithNoSoftwareModules("ds2", "1.0"); + // verify quota is exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> distributionSetManagement.assignSoftwareModules(ds2.getId(), modules)); + + // assign some software modules + final DistributionSet ds3 = testdataFactory.createDistributionSetWithNoSoftwareModules("ds3", "1.0"); + final int firstHalf = Math.round(maxModules / 2); + for (int i = 0; i < firstHalf; ++i) { + distributionSetManagement.assignSoftwareModules(ds3.getId(), Collections.singletonList(modules.get(i))); + } + // assign the remaining modules to cause the quota to be exceeded + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> distributionSetManagement + .assignSoftwareModules(ds3.getId(), modules.subList(firstHalf, modules.size()))); + + } + @Test @WithUser(allSpPermissions = true) @Description("Checks that metadata for a distribution set can be updated.") @@ -491,7 +578,7 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { final Iterator dsIterator = buildDistributionSets.iterator(); final Iterator tIterator = buildTargetFixtures.iterator(); - final DistributionSet dsFirst = dsIterator.next(); + dsIterator.next(); final DistributionSet dsSecond = dsIterator.next(); final DistributionSet dsThree = dsIterator.next(); final DistributionSet dsFour = dsIterator.next(); @@ -775,7 +862,7 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { new JpaDistributionSetMetadata("key" + index, ds1, "value" + index)); } - for (int index = 0; index < 20; index++) { + for (int index = 0; index < 8; index++) { createDistributionSetMetadata(ds2.getId(), new JpaDistributionSetMetadata("key" + index, ds2, "value" + index)); } @@ -789,8 +876,8 @@ public class DistributionSetManagementTest extends AbstractJpaIntegrationTest { assertThat(metadataOfDs1.getNumberOfElements()).isEqualTo(10); assertThat(metadataOfDs1.getTotalElements()).isEqualTo(10); - assertThat(metadataOfDs2.getNumberOfElements()).isEqualTo(20); - assertThat(metadataOfDs2.getTotalElements()).isEqualTo(20); + assertThat(metadataOfDs2.getNumberOfElements()).isEqualTo(8); + assertThat(metadataOfDs2.getTotalElements()).isEqualTo(8); } @Test diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTypeManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTypeManagementTest.java index e9775cffd..2f42ac569 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTypeManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/DistributionSetTypeManagementTest.java @@ -8,17 +8,26 @@ */ package org.eclipse.hawkbit.repository.jpa; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import java.util.Arrays; +import java.util.Collections; +import java.util.List; import javax.validation.ConstraintViolationException; import org.apache.commons.lang3.RandomStringUtils; +import org.assertj.core.util.Lists; import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.builder.SoftwareModuleTypeCreate; 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.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; @@ -28,10 +37,6 @@ import org.junit.Test; import com.google.common.collect.Sets; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import ru.yandex.qatools.allure.annotations.Description; import ru.yandex.qatools.allure.annotations.Features; import ru.yandex.qatools.allure.annotations.Step; @@ -193,7 +198,7 @@ public class DistributionSetTypeManagementTest extends AbstractJpaIntegrationTes } @Test - @Description("Tests the successfull module update of unused distribution set type which is in fact allowed.") + @Description("Tests the successful module update of unused distribution set type which is in fact allowed.") public void updateUnassignedDistributionSetTypeModules() { final DistributionSetType updatableType = distributionSetTypeManagement .create(entityFactory.distributionSetType().create().key("updatableType").name("to be deleted")); @@ -217,6 +222,55 @@ public class DistributionSetTypeManagementTest extends AbstractJpaIntegrationTes .containsOnly(runtimeType); } + @Test + @Description("Verifies that the quota for software module types per distribution set type is enforced as expected.") + public void quotaMaxSoftwareModuleTypes() { + + final int quota = quotaManagement.getMaxSoftwareModuleTypesPerDistributionSetType(); + // create software module types + final List moduleTypeIds = Lists.newArrayList(); + for (int i = 0; i < quota + 1; ++i) { + final SoftwareModuleTypeCreate smCreate = entityFactory.softwareModuleType().create().name("smType_" + i) + .description("smType_" + i).maxAssignments(1).colour("blue").key("smType_" + i); + moduleTypeIds.add(softwareModuleTypeManagement.create(smCreate).getId()); + } + + // assign all types at once + final DistributionSetType dsType1 = distributionSetTypeManagement + .create(entityFactory.distributionSetType().create().key("dst1").name("dst1")); + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy( + () -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType1.getId(), moduleTypeIds)); + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy( + () -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(dsType1.getId(), moduleTypeIds)); + + // assign as many mandatory modules as possible + final DistributionSetType dsType2 = distributionSetTypeManagement + .create(entityFactory.distributionSetType().create().key("dst2").name("dst2")); + distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType2.getId(), + moduleTypeIds.subList(0, quota)); + assertThat(distributionSetTypeManagement.get(dsType2.getId())).isNotEmpty(); + assertThat(distributionSetTypeManagement.get(dsType2.getId()).get().getMandatoryModuleTypes().size()) + .isEqualTo(quota); + // assign one more to trigger the quota exceeded error + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType2.getId(), + Collections.singletonList(moduleTypeIds.get(quota)))); + + // assign as many optional modules as possible + final DistributionSetType dsType3 = distributionSetTypeManagement + .create(entityFactory.distributionSetType().create().key("dst3").name("dst3")); + distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(dsType3.getId(), + moduleTypeIds.subList(0, quota)); + assertThat(distributionSetTypeManagement.get(dsType3.getId())).isNotEmpty(); + assertThat(distributionSetTypeManagement.get(dsType3.getId()).get().getOptionalModuleTypes().size()) + .isEqualTo(quota); + // assign one more to trigger the quota exceeded error + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> distributionSetTypeManagement.assignOptionalSoftwareModuleTypes(dsType3.getId(), + Collections.singletonList(moduleTypeIds.get(quota)))); + + } + @Test @Description("Tests the successfull update of used distribution set type meta data which is in fact allowed.") public void updateAssignedDistributionSetTypeMetaData() { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java index bc2431cc7..33ba52992 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java @@ -8,6 +8,9 @@ */ package org.eclipse.hawkbit.repository.jpa; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -39,6 +42,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetUpdatedEvent; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityReadOnlyException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; @@ -70,9 +74,6 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import ru.yandex.qatools.allure.annotations.Description; import ru.yandex.qatools.allure.annotations.Features; import ru.yandex.qatools.allure.annotations.Step; @@ -1257,6 +1258,87 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { validateRolloutActionStatus(myRollout.getId(), expectedTargetCountStatus); } + @Test + @Description("Verify that a rollout cannot be created if the 'max targets per rollout group' quota is violated.") + public void createRolloutFailsIfQuotaGroupQuotaIsViolated() throws Exception { + + final int maxTargets = quotaManagement.getMaxTargetsPerRolloutGroup(); + + final int amountTargetsForRollout = maxTargets + 1; + final int amountGroups = 1; + final String successCondition = "50"; + final String errorCondition = "80"; + final String rolloutName = "rolloutTest"; + final String targetPrefixName = rolloutName; + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + testdataFactory.createTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName); + + final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults() + .successCondition(RolloutGroupSuccessCondition.THRESHOLD, successCondition) + .errorCondition(RolloutGroupErrorCondition.THRESHOLD, errorCondition) + .errorAction(RolloutGroupErrorAction.PAUSE, null).build(); + + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> rolloutManagement.create( + entityFactory.rollout().create().name(rolloutName).description(rolloutName) + .targetFilterQuery("controllerId==" + targetPrefixName + "-*").set(distributionSet), + amountGroups, conditions)); + + } + + @Test + @Description("Verify that a rollout cannot be created based on group definitions if the 'max targets per rollout group' quota is violated for one of the groups.") + public void createRolloutWithGroupDefinitionsFailsIfQuotaGroupQuotaIsViolated() throws Exception { + + final int maxTargets = quotaManagement.getMaxTargetsPerRolloutGroup(); + + final int amountTargetsForRollout = maxTargets * 2 + 2; + final String rolloutName = "rolloutTest"; + final String targetPrefixName = rolloutName; + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + testdataFactory.createTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName); + + final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults().build(); + + // create group definitions + final RolloutGroupCreate group1 = entityFactory.rolloutGroup().create().conditions(conditions).name("group1") + .targetPercentage(50.0F); + final RolloutGroupCreate group2 = entityFactory.rolloutGroup().create().conditions(conditions).name("group2") + .targetPercentage(100.0F); + + // group1 exceeds the quota + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> rolloutManagement.create( + entityFactory.rollout().create().name(rolloutName).description(rolloutName) + .targetFilterQuery("controllerId==" + targetPrefixName + "-*").set(distributionSet), + Arrays.asList(group1, group2), conditions)); + + // create group definitions + final RolloutGroupCreate group3 = entityFactory.rolloutGroup().create().conditions(conditions).name("group3") + .targetPercentage(1.0F); + final RolloutGroupCreate group4 = entityFactory.rolloutGroup().create().conditions(conditions).name("group4") + .targetPercentage(100.0F); + + // group4 exceeds the quota + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> rolloutManagement.create( + entityFactory.rollout().create().name(rolloutName).description(rolloutName) + .targetFilterQuery("controllerId==" + targetPrefixName + "-*").set(distributionSet), + Arrays.asList(group3, group4), conditions)); + + // create group definitions + final RolloutGroupCreate group5 = entityFactory.rolloutGroup().create().conditions(conditions).name("group5") + .targetPercentage(33.3F); + final RolloutGroupCreate group6 = entityFactory.rolloutGroup().create().conditions(conditions).name("group6") + .targetPercentage(66.6F); + final RolloutGroupCreate group7 = entityFactory.rolloutGroup().create().conditions(conditions).name("group7") + .targetPercentage(100.0F); + + // should work fine + assertThat(rolloutManagement.create( + entityFactory.rollout().create().name(rolloutName).description(rolloutName) + .targetFilterQuery("controllerId==" + targetPrefixName + "-*").set(distributionSet), + Arrays.asList(group5, group6, group7), conditions)).isNotNull(); + + } + @Test @Description("Verify the creation and the automatic start of a rollout.") public void createAndAutoStartRollout() throws Exception { @@ -1308,7 +1390,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Verify the creation of a Rollout with a groups definition.") + @Description("Verify the creation of a rollout with a groups definition.") public void createRolloutWithGroupDefinition() throws Exception { final String rolloutName = "rolloutTest3"; @@ -1355,7 +1437,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { assertThat(myRollout.getTotalTargets()).isEqualTo(amountTargetsInGroup1and2 + amountTargetsInGroup1); final List groups = rolloutGroupManagement.findByRollout(PAGE, myRollout.getId()).getContent(); - ; + assertThat(groups.get(0).getStatus()).isEqualTo(RolloutGroupStatus.READY); assertThat(groups.get(0).getTotalTargets()).isEqualTo(amountTargetsInGroup1); @@ -1368,7 +1450,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Verify Exception when a Rollout with Group definition is created that does not address all targets") + @Description("Verify rollout creation fails if group definition does not address all targets") public void createRolloutWithGroupsNotMatchingTargets() throws Exception { final String rolloutName = "rolloutTest4"; final int amountTargetsForRollout = 500; @@ -1389,7 +1471,7 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Verify Exception when a Rollout with Group definition is created that contains an illegal percentage") + @Description("Verify rollout creation fails if group definition specifies illegal target percentage") public void createRolloutWithIllegalPercentage() throws Exception { final String rolloutName = "rolloutTest6"; final int amountTargetsForRollout = 10; @@ -1410,18 +1492,18 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { } @Test - @Description("Verify Exception when a Rollout is created with too much groups") + @Description("Verify rollout creation fails if the 'max rollout groups per rollout' quota is violated.") public void createRolloutWithIllegalAmountOfGroups() throws Exception { final String rolloutName = "rolloutTest5"; - final int amountTargetsForRollout = 10; - final int illegalGroupAmount = 501; + final int targets = 10; + final int maxGroups = quotaManagement.getMaxRolloutGroupsPerRollout(); final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults().build(); - final RolloutCreate myRollout = generateTargetsAndRollout(rolloutName, amountTargetsForRollout); + final RolloutCreate rollout = generateTargetsAndRollout(rolloutName, targets); - assertThatExceptionOfType(ValidationException.class) - .isThrownBy(() -> rolloutManagement.create(myRollout, illegalGroupAmount, conditions)) - .withMessageContaining("not be greater than " + quotaManagement.getMaxRolloutGroupsPerRollout()); + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> rolloutManagement.create(rollout, maxGroups + 1, conditions)) + .withMessageContaining("not be greater than " + maxGroups); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleManagementTest.java index d6b0f6703..e95e767d1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SoftwareModuleManagementTest.java @@ -15,6 +15,7 @@ import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -23,6 +24,7 @@ import org.apache.commons.lang3.RandomUtils; import org.eclipse.hawkbit.repository.builder.SoftwareModuleMetadataCreate; import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedEvent; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleMetadata; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; @@ -361,13 +363,14 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { public void deleteSoftwareModulesWithSharedArtifact() throws IOException { // Init artifact binary data, target and DistributionSets - final byte[] source = RandomUtils.nextBytes(1024); + final int artifactSize = 1024; + final byte[] source = RandomUtils.nextBytes(artifactSize); // [STEP1]: Create SoftwareModuleX and add a new ArtifactX SoftwareModule moduleX = createSoftwareModuleWithArtifacts(osType, "modulex", "v1.0", 0); // [STEP2]: Create newArtifactX and add it to SoftwareModuleX - artifactManagement.create(new ByteArrayInputStream(source), moduleX.getId(), "artifactx", false); + artifactManagement.create(new ByteArrayInputStream(source), moduleX.getId(), "artifactx", false, artifactSize); moduleX = softwareModuleManagement.get(moduleX.getId()).get(); final Artifact artifactX = moduleX.getArtifacts().iterator().next(); @@ -375,7 +378,7 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { SoftwareModule moduleY = createSoftwareModuleWithArtifacts(osType, "moduley", "v1.0", 0); // [STEP4]: Assign the same ArtifactX to SoftwareModuleY - artifactManagement.create(new ByteArrayInputStream(source), moduleY.getId(), "artifactx", false); + artifactManagement.create(new ByteArrayInputStream(source), moduleY.getId(), "artifactx", false, artifactSize); moduleY = softwareModuleManagement.get(moduleY.getId()).get(); final Artifact artifactY = moduleY.getArtifacts().iterator().next(); @@ -403,20 +406,21 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { public void deleteMultipleSoftwareModulesWhichShareAnArtifact() throws IOException { // Init artifact binary data, target and DistributionSets - final byte[] source = RandomUtils.nextBytes(1024); + final int artifactSize = 1024; + final byte[] source = RandomUtils.nextBytes(artifactSize); final Target target = testdataFactory.createTarget(); // [STEP1]: Create SoftwareModuleX and add a new ArtifactX SoftwareModule moduleX = createSoftwareModuleWithArtifacts(osType, "modulex", "v1.0", 0); - artifactManagement.create(new ByteArrayInputStream(source), moduleX.getId(), "artifactx", false); + artifactManagement.create(new ByteArrayInputStream(source), moduleX.getId(), "artifactx", false, artifactSize); moduleX = softwareModuleManagement.get(moduleX.getId()).get(); final Artifact artifactX = moduleX.getArtifacts().iterator().next(); // [STEP2]: Create SoftwareModuleY and add the same ArtifactX SoftwareModule moduleY = createSoftwareModuleWithArtifacts(osType, "moduley", "v1.0", 0); - artifactManagement.create(new ByteArrayInputStream(source), moduleY.getId(), "artifactx", false); + artifactManagement.create(new ByteArrayInputStream(source), moduleY.getId(), "artifactx", false, artifactSize); moduleY = softwareModuleManagement.get(moduleY.getId()).get(); final Artifact artifactY = moduleY.getArtifacts().iterator().next(); @@ -462,9 +466,10 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { SoftwareModule softwareModule = softwareModuleManagement.create(entityFactory.softwareModule().create() .type(type).name(name).version(version).description("description of artifact " + name)); + final int artifactSize = 5 * 1024; for (int i = 0; i < numberArtifacts; i++) { - artifactManagement.create(new RandomGeneratedInputStream(5 * 1024), softwareModule.getId(), - "file" + (i + 1), false); + artifactManagement.create(new RandomGeneratedInputStream(artifactSize), softwareModule.getId(), + "file" + (i + 1), false, artifactSize); } // Verify correct Creation of SoftwareModule and corresponding artifacts @@ -652,14 +657,58 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { assertThat(softwareModuleMetadata.get(0).getEntityId()).isEqualTo(ah.getId()); } + @Test + @Description("Verifies the enforcement of the metadata quota per software module.") + public void createSoftwareModuleMetadataUntilQuotaIsExceeded() { + + // add meta data one by one + final SoftwareModule module = testdataFactory.createSoftwareModuleApp("m1"); + final int maxMetaData = quotaManagement.getMaxMetaDataEntriesPerSoftwareModule(); + for (int i = 0; i < maxMetaData; ++i) { + softwareModuleManagement.createMetaData( + entityFactory.softwareModuleMetadata().create(module.getId()).key("k" + i).value("v" + i)); + } + + // quota exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata() + .create(module.getId()).key("k" + maxMetaData).value("v" + maxMetaData))); + + // add multiple meta data entries at once + final SoftwareModule module2 = testdataFactory.createSoftwareModuleApp("m2"); + final List create = new ArrayList<>(); + for (int i = 0; i < maxMetaData + 1; ++i) { + create.add(entityFactory.softwareModuleMetadata().create(module2.getId()).key("k" + i).value("v" + i)); + } + // quota exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> softwareModuleManagement.createMetaData(create)); + + // add some meta data entries + final SoftwareModule module3 = testdataFactory.createSoftwareModuleApp("m3"); + final int firstHalf = Math.round(maxMetaData / 2); + for (int i = 0; i < firstHalf; ++i) { + softwareModuleManagement.createMetaData( + entityFactory.softwareModuleMetadata().create(module3.getId()).key("k" + i).value("v" + i)); + } + // add too many data entries + final int secondHalf = maxMetaData - firstHalf; + final List create2 = new ArrayList<>(); + for (int i = 0; i < secondHalf + 1; ++i) { + create2.add(entityFactory.softwareModuleMetadata().create(module3.getId()).key("kk" + i).value("vv" + i)); + } + // quota exceeded + assertThatExceptionOfType(QuotaExceededException.class) + .isThrownBy(() -> softwareModuleManagement.createMetaData(create2)); + + } + @Test @Description("Checks that metadata for a software module cannot be created for an existing key.") public void createSoftwareModuleMetadataFailsIfKeyExists() { final String knownKey1 = "myKnownKey1"; final String knownValue1 = "myKnownValue1"; - final String knownValue2 = "myKnownValue2"; - final SoftwareModule ah = testdataFactory.createSoftwareModuleApp(); softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(ah.getId()).key(knownKey1) @@ -765,41 +814,43 @@ public class SoftwareModuleManagementTest extends AbstractJpaIntegrationTest { public void findAllSoftwareModuleMetadataBySwId() { final SoftwareModule sw1 = testdataFactory.createSoftwareModuleApp(); + final int metadataCountSw1 = 8; final SoftwareModule sw2 = testdataFactory.createSoftwareModuleOs(); + final int metadataCountSw2 = 10; - for (int index = 0; index < 10; index++) { + for (int index = 0; index < metadataCountSw1; index++) { softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(sw1.getId()) .key("key" + index).value("value" + index).targetVisible(true)); } - for (int index = 0; index < 20; index++) { + for (int index = 0; index < metadataCountSw2; index++) { softwareModuleManagement.createMetaData(entityFactory.softwareModuleMetadata().create(sw2.getId()) .key("key" + index).value("value" + index).targetVisible(false)); } - Page metadataOfSw1 = softwareModuleManagement + Page metadataSw1 = softwareModuleManagement .findMetaDataBySoftwareModuleId(new PageRequest(0, 100), sw1.getId()); - Page metadataOfSw2 = softwareModuleManagement + Page metadataSw2 = softwareModuleManagement .findMetaDataBySoftwareModuleId(new PageRequest(0, 100), sw2.getId()); - assertThat(metadataOfSw1.getNumberOfElements()).isEqualTo(10); - assertThat(metadataOfSw1.getTotalElements()).isEqualTo(10); + assertThat(metadataSw1.getNumberOfElements()).isEqualTo(metadataCountSw1); + assertThat(metadataSw1.getTotalElements()).isEqualTo(metadataCountSw1); - assertThat(metadataOfSw2.getNumberOfElements()).isEqualTo(20); - assertThat(metadataOfSw2.getTotalElements()).isEqualTo(20); + assertThat(metadataSw2.getNumberOfElements()).isEqualTo(metadataCountSw2); + assertThat(metadataSw2.getTotalElements()).isEqualTo(metadataCountSw2); - metadataOfSw1 = softwareModuleManagement.findMetaDataBySoftwareModuleIdAndTargetVisible(new PageRequest(0, 100), + metadataSw1 = softwareModuleManagement.findMetaDataBySoftwareModuleIdAndTargetVisible(new PageRequest(0, 100), sw1.getId()); - metadataOfSw2 = softwareModuleManagement.findMetaDataBySoftwareModuleIdAndTargetVisible(new PageRequest(0, 100), + metadataSw2 = softwareModuleManagement.findMetaDataBySoftwareModuleIdAndTargetVisible(new PageRequest(0, 100), sw2.getId()); - assertThat(metadataOfSw1.getNumberOfElements()).isEqualTo(10); - assertThat(metadataOfSw1.getTotalElements()).isEqualTo(10); + assertThat(metadataSw1.getNumberOfElements()).isEqualTo(metadataCountSw1); + assertThat(metadataSw1.getTotalElements()).isEqualTo(metadataCountSw1); - assertThat(metadataOfSw2.getNumberOfElements()).isEqualTo(0); - assertThat(metadataOfSw2.getTotalElements()).isEqualTo(0); + assertThat(metadataSw2.getNumberOfElements()).isEqualTo(0); + assertThat(metadataSw2.getTotalElements()).isEqualTo(0); } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SystemManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SystemManagementTest.java index 09f004b6a..8eefabc41 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SystemManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SystemManagementTest.java @@ -134,13 +134,13 @@ public class SystemManagementTest extends AbstractJpaIntegrationTest { private void createTestArtifact(final byte[] random) { final SoftwareModule sm = testdataFactory.createSoftwareModuleOs(); - artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", false); + artifactManagement.create(new ByteArrayInputStream(random), sm.getId(), "file1", false, random.length); } private void createDeletedTestArtifact(final byte[] random) { final DistributionSet ds = testdataFactory.createDistributionSet("deleted garbage", true); ds.getModules().stream().forEach(module -> { - artifactManagement.create(new ByteArrayInputStream(random), module.getId(), "file1", false); + artifactManagement.create(new ByteArrayInputStream(random), module.getId(), "file1", false, random.length); softwareModuleManagement.delete(module.getId()); }); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java index 753fbe26e..dee9892cb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetFilterQueryManagementTest.java @@ -8,6 +8,8 @@ */ package org.eclipse.hawkbit.repository.jpa; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -24,6 +26,7 @@ import org.eclipse.hawkbit.repository.event.remote.entity.SoftwareModuleCreatedE import org.eclipse.hawkbit.repository.event.remote.entity.TargetCreatedEvent; import org.eclipse.hawkbit.repository.event.remote.entity.TargetFilterQueryCreatedEvent; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; @@ -34,8 +37,6 @@ import org.junit.Test; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import static org.assertj.core.api.Assertions.assertThat; - import ru.yandex.qatools.allure.annotations.Description; import ru.yandex.qatools.allure.annotations.Features; import ru.yandex.qatools.allure.annotations.Stories; @@ -97,6 +98,20 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest targetFilterQueryManagement.getByName(filterName).get()); } + @Test + @Description("Create a target filter query with an auto-assign distribution set and a query string that addresses too many targets.") + public void createTargetFilterQueryThatExceedsQuota() { + + // create targets + final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment(); + testdataFactory.createTargets(maxTargets + 1, "target%s"); + final DistributionSet set = testdataFactory.createDistributionSet(); + + // creation is supposed to work as there is no distribution set + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> targetFilterQueryManagement.create( + entityFactory.targetFilterQuery().create().name("testfilter").set(set.getId()).query("name==target*"))); + } + @Test @Description("Test searching a target filter query.") public void searchTargetFilterQuery() { @@ -181,6 +196,43 @@ public class TargetFilterQueryManagementTest extends AbstractJpaIntegrationTest } + @Test + @Description("Assigns a distribution set to an existing filter query and verifies that the quota 'max targets per auto assignment' is enforced.") + public void assignDistributionSetToTargetFilterQueryThatExceedsQuota() { + + // create targets + final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment(); + testdataFactory.createTargets(maxTargets + 1, "target%s"); + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + + // creation is supposed to work as there is no distribution set + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("testfilter").query("name==target*")); + + // assigning a distribution set is supposed to fail as the query + // addresses too many targets + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> targetFilterQueryManagement + .updateAutoAssignDS(targetFilterQuery.getId(), distributionSet.getId())); + } + + @Test + @Description("Updates an existing filter query with a query string that addresses too many targets.") + public void updateTargetFilterQueryWithQueryThatExceedsQuota() { + + // create targets + final int maxTargets = quotaManagement.getMaxTargetsPerAutoAssignment(); + testdataFactory.createTargets(maxTargets + 1, "target%s"); + final DistributionSet set = testdataFactory.createDistributionSet(); + + // creation is supposed to work as the query does not exceed the quota + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement.create( + entityFactory.targetFilterQuery().create().name("testfilter").set(set.getId()).query("name==foo")); + + // update with a query string that addresses too many targets + assertThatExceptionOfType(QuotaExceededException.class).isThrownBy(() -> targetFilterQueryManagement + .update(entityFactory.targetFilterQuery().update(targetFilterQuery.getId()).query("name==target*"))); + } + @Test @Description("Test removing distribution set while it has a relation to a target filter query") public void removeAssignDistributionSet() { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties b/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties index a5bf32f47..1910b49b1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties @@ -10,6 +10,15 @@ # Quota - START hawkbit.server.security.dos.maxStatusEntriesPerAction=10 hawkbit.server.security.dos.maxAttributeEntriesPerTarget=10 +hawkbit.server.security.dos.maxMetaDataEntriesPerSoftwareModule=10 +hawkbit.server.security.dos.maxRolloutGroupsPerRollout=20 +hawkbit.server.security.dos.maxMessagesPerActionStatus=10 +hawkbit.server.security.dos.maxMetaDataEntriesPerDistributionSet=10 +hawkbit.server.security.dos.maxSoftwareModuleTypesPerDistributionSetType=10 +hawkbit.server.security.dos.maxSoftwareModulesPerDistributionSet=10 +hawkbit.server.security.dos.maxArtifactsPerSoftwareModule=10 +hawkbit.server.security.dos.maxTargetsPerRolloutGroup=1000 +hawkbit.server.security.dos.maxArtifactSize=1000000 # Quota - END # Debug utility functions - START diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index 70f42d662..50d91cbb2 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -276,7 +276,11 @@ public abstract class AbstractIntegrationTest { } protected DistributionSetMetadata createDistributionSetMetadata(final Long dsId, final MetaData md) { - return distributionSetManagement.createMetaData(dsId, Collections.singletonList(md)).get(0); + return createDistributionSetMetadata(dsId, Collections.singletonList(md)).get(0); + } + + protected List createDistributionSetMetadata(final Long dsId, final List md) { + return distributionSetManagement.createMetaData(dsId, md); } protected Long getOsModule(final DistributionSet ds) { diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index 785a32e63..3699f9ec4 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -463,8 +463,10 @@ public class TestdataFactory { public List createArtifacts(final Long moduleId) { final List artifacts = new ArrayList<>(); for (int i = 0; i < 3; i++) { - final InputStream stubInputStream = IOUtils.toInputStream("some test data" + i, Charset.forName("UTF-8")); - artifacts.add(artifactManagement.create(stubInputStream, moduleId, "filename" + i, false)); + final String artifactData = "some test data" + i; + final InputStream stubInputStream = IOUtils.toInputStream(artifactData, Charset.forName("UTF-8")); + artifacts.add( + artifactManagement.create(stubInputStream, moduleId, "filename" + i, false, artifactData.length())); } diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java index de2ab6d4f..879974b5d 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java @@ -114,6 +114,58 @@ public class HawkbitSecurityProperties { */ private int maxMessagesPerActionStatus = 50; + /** + * Maximum number of meta data entries per software module + */ + private int maxMetaDataEntriesPerSoftwareModule = 100; + + /** + * Maximum number of meta data entries per distribution set + */ + private int maxMetaDataEntriesPerDistributionSet = 100; + + /** + * Maximum number of software modules per distribution set + */ + private int maxSoftwareModulesPerDistributionSet = 100; + + /** + * Maximum number of software modules per distribution set + */ + private int maxSoftwareModuleTypesPerDistributionSetType = 50; + + /** + * Maximum number of artifacts per software module + */ + private int maxArtifactsPerSoftwareModule = 50; + + /** + * Maximum number of targets per rollout group + */ + private int maxTargetsPerRolloutGroup = 20000; + + /** + * Maximum number of targets per rollout group + */ + private int maxActionsPerTarget = 500; + + /** + * Maximum number of targets for a manual distribution set assignment. + * Must be greater than 1000. + */ + private int maxTargetsPerManualAssignment = 5000; + + /** + * Maximum number of targets for an automatic distribution set + * assignment + */ + private int maxTargetsPerAutoAssignment = 5000; + + /** + * Maximum size of artifacts in bytes. + */ + private long maxArtifactSize = 1_000_000_000; + private final Filter filter = new Filter(); private final Filter uiFilter = new Filter(); @@ -157,6 +209,87 @@ public class HawkbitSecurityProperties { this.maxRolloutGroupsPerRollout = maxRolloutGroupsPerRollout; } + public int getMaxMetaDataEntriesPerSoftwareModule() { + return maxMetaDataEntriesPerSoftwareModule; + } + + public void setMaxMetaDataEntriesPerSoftwareModule(final int maxMetaDataEntriesPerSoftwareModule) { + this.maxMetaDataEntriesPerSoftwareModule = maxMetaDataEntriesPerSoftwareModule; + } + + public int getMaxMetaDataEntriesPerDistributionSet() { + return maxMetaDataEntriesPerDistributionSet; + } + + public void setMaxMetaDataEntriesPerDistributionSet(final int maxMetaDataEntriesPerDistributionSet) { + this.maxMetaDataEntriesPerDistributionSet = maxMetaDataEntriesPerDistributionSet; + } + + public int getMaxSoftwareModulesPerDistributionSet() { + return maxSoftwareModulesPerDistributionSet; + } + + public void setMaxSoftwareModulesPerDistributionSet(final int maxSoftwareModulesPerDistributionSet) { + this.maxSoftwareModulesPerDistributionSet = maxSoftwareModulesPerDistributionSet; + } + + public int getMaxSoftwareModuleTypesPerDistributionSetType() { + return maxSoftwareModuleTypesPerDistributionSetType; + } + + public void setMaxSoftwareModuleTypesPerDistributionSetType( + final int maxSoftwareModuleTypesPerDistributionSetType) { + this.maxSoftwareModuleTypesPerDistributionSetType = maxSoftwareModuleTypesPerDistributionSetType; + } + + public int getMaxArtifactsPerSoftwareModule() { + return maxArtifactsPerSoftwareModule; + } + + public void setMaxArtifactsPerSoftwareModule(final int maxArtifactsPerSoftwareModule) { + this.maxArtifactsPerSoftwareModule = maxArtifactsPerSoftwareModule; + } + + public int getMaxTargetsPerRolloutGroup() { + return maxTargetsPerRolloutGroup; + } + + public void setMaxTargetsPerRolloutGroup(final int maxTargetsPerRolloutGroup) { + this.maxTargetsPerRolloutGroup = maxTargetsPerRolloutGroup; + } + + public int getMaxActionsPerTarget() { + return maxActionsPerTarget; + } + + public void setMaxActionsPerTarget(final int maxActionsPerTarget) { + this.maxActionsPerTarget = maxActionsPerTarget; + } + + public int getMaxTargetsPerManualAssignment() { + return maxTargetsPerManualAssignment; + } + + public void setMaxTargetsPerManualAssignment(final int maxTargetsPerManualAssignment) { + this.maxTargetsPerManualAssignment = maxTargetsPerManualAssignment; + } + + public int getMaxTargetsPerAutoAssignment() { + return maxTargetsPerAutoAssignment; + } + + public void setMaxTargetsPerAutoAssignment(final int maxTargetsPerAutoAssignment) { + this.maxTargetsPerAutoAssignment = maxTargetsPerAutoAssignment; + } + + public void setMaxArtifactSize(final long maxArtifactSize) { + this.maxArtifactSize = maxArtifactSize; + } + + public long getMaxArtifactSize() { + return maxArtifactSize; + } + /** * Configuration for hawkBits DOS prevention filter. This is usually an * infrastructure topic (e.g. Web Application Firewall (WAF)) but might @@ -179,13 +312,13 @@ public class HawkbitSecurityProperties { * # Maximum number of allowed REST read/GET requests per second per * client IP. */ - int maxRead = 200; + private int maxRead = 200; /** * Maximum number of allowed REST write/(PUT/POST/etc.) requests per * second per client IP. */ - int maxWrite = 50; + private int maxWrite = 50; public boolean isEnabled() { return enabled; @@ -220,5 +353,6 @@ public class HawkbitSecurityProperties { } } + } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadConfirmationWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadConfirmationWindow.java index 2073dc61a..23e54796a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadConfirmationWindow.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/upload/UploadConfirmationWindow.java @@ -20,6 +20,7 @@ import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.exception.ArtifactUploadFailedException; import org.eclipse.hawkbit.repository.exception.InvalidMD5HashException; import org.eclipse.hawkbit.repository.exception.InvalidSHA1HashException; +import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; @@ -591,7 +592,8 @@ public class UploadConfirmationWindow implements Button.ClickListener { customFile.getBaseSoftwareModuleName(), customFile.getBaseSoftwareModuleVersion()); if (customFile.getFileName().equals(fileName) && baseSwModuleNameVersion.equals(baseSoftwareModuleNameVersion)) { - createArtifact(itemId, customFile.getFilePath(), artifactManagement, bSoftwareModule); + createArtifact(itemId, customFile.getFilePath(), customFile.getFileSize(), artifactManagement, + bSoftwareModule); } } refreshArtifactDetailsLayout = checkIfArtifactDetailsDisplayed(bSoftwareModule.getId()); @@ -618,8 +620,8 @@ public class UploadConfirmationWindow implements Button.ClickListener { currentUploadResultWindow = null; } - private void createArtifact(final String itemId, final String filePath, final ArtifactManagement artifactManagement, - final SoftwareModule baseSw) { + private void createArtifact(final String itemId, final String filePath, final long fileSize, + final ArtifactManagement artifactManagement, final SoftwareModule baseSw) { final File newFile = new File(filePath); final Item item = tableContainer.getItem(itemId); @@ -638,11 +640,11 @@ public class UploadConfirmationWindow implements Button.ClickListener { try (FileInputStream fis = new FileInputStream(newFile)) { artifactManagement.create(fis, baseSw.getId(), providedFileName, md5Checksum, sha1Checksum, true, - customFile.getMimeType()); + customFile.getMimeType(), fileSize); saveUploadStatus(providedFileName, swModuleNameVersion, SPUILabelDefinitions.SUCCESS, ""); } catch (final ArtifactUploadFailedException | InvalidSHA1HashException | InvalidMD5HashException - | FileNotFoundException e) { + | FileNotFoundException | QuotaExceededException e) { saveUploadStatus(providedFileName, swModuleNameVersion, SPUILabelDefinitions.FAILED, e.getMessage()); LOG.error(ARTIFACT_UPLOAD_EXCEPTION, e); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java index 5e1c0a62e..556b21c95 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java @@ -91,14 +91,17 @@ import com.vaadin.ui.themes.ValoTheme; /** * Rollout add or update popup layout. */ +@SuppressWarnings({ "squid:MaximumInheritanceDepth", "squid:S2160" }) public class AddUpdateRolloutWindowLayout extends GridLayout { - private static final long serialVersionUID = 2999293468801479916L; + private static final long serialVersionUID = 1L; private static final Logger LOGGER = LoggerFactory.getLogger(AddUpdateRolloutWindowLayout.class); private static final String MESSAGE_ROLLOUT_FIELD_VALUE_RANGE = "message.rollout.field.value.range"; + private static final String MESSAGE_ROLLOUT_MAX_GROUP_SIZE_EXCEEDED = "message.rollout.max.group.size.exceeded"; + private static final String MESSAGE_ROLLOUT_FILTER_TARGET_EXISTS = "message.rollout.filter.target.exists"; private static final String MESSAGE_ENTER_NUMBER = "message.enter.number"; @@ -167,6 +170,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private final NullValidator nullValidator = new NullValidator(null, false); + @SuppressWarnings("squid:S00107") AddUpdateRolloutWindowLayout(final RolloutManagement rolloutManagement, final TargetManagement targetManagement, final UINotification uiNotification, final UiProperties uiProperties, final EntityFactory entityFactory, final VaadinMessageSource i18n, final UIEventBus eventBus, @@ -339,7 +343,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private int getErrorThresholdPercentage(final int amountGroup) { int errorThresoldPercent = Integer.parseInt(errorThreshold.getValue()); - if (errorThresholdOptionGroup.getValue().equals(ERRORTHRESOLDOPTIONS.COUNT.getValue())) { + if (errorThresholdOptionGroup.getValue().equals(ERROR_THRESHOLD_OPTIONS.COUNT.getValue())) { final int groupSize = (int) Math.ceil((double) totalTargetsCount / (double) amountGroup); final int erroThresoldCount = Integer.parseInt(errorThreshold.getValue()); errorThresoldPercent = (int) Math.ceil(((float) erroThresoldCount / (float) groupSize) * 100); @@ -626,7 +630,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private OptionGroup createErrorThresholdOptionGroup() { final OptionGroup errorThresoldOptions = new OptionGroup(); - for (final ERRORTHRESOLDOPTIONS option : ERRORTHRESOLDOPTIONS.values()) { + for (final ERROR_THRESHOLD_OPTIONS option : ERROR_THRESHOLD_OPTIONS.values()) { errorThresoldOptions.addItem(option.getValue()); } errorThresoldOptions.setId(UIComponentIdProvider.ROLLOUT_ERROR_THRESOLD_OPTION_ID); @@ -640,8 +644,8 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private void listenerOnErrorThresoldOptionChange(final ValueChangeEvent event) { errorThreshold.clear(); errorThreshold.removeAllValidators(); - if (event.getProperty().getValue().equals(ERRORTHRESOLDOPTIONS.COUNT.getValue())) { - errorThreshold.addValidator(new ErrorThresoldOptionValidator()); + if (event.getProperty().getValue().equals(ERROR_THRESHOLD_OPTIONS.COUNT.getValue())) { + errorThreshold.addValidator(new ErrorThresholdOptionValidator()); } else { errorThreshold.addValidator(new ThresholdFieldValidator()); } @@ -732,6 +736,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { groupsLegendLayout.populateTotalTargets(totalTargetsCount); defineGroupsLayout.setTargetFilter(filterQueryString); } + noOfGroups.markAsDirty(); onGroupNumberChange(event); } @@ -771,7 +776,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } private void setDefaultSaveStartGroupOption() { - errorThresholdOptionGroup.setValue(ERRORTHRESOLDOPTIONS.PERCENT.getValue()); + errorThresholdOptionGroup.setValue(ERROR_THRESHOLD_OPTIONS.PERCENT.getValue()); } private static TextArea createDescription() { @@ -802,6 +807,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { final TextField noOfGroupsField = createIntegerTextField("prompt.number.of.groups", UIComponentIdProvider.ROLLOUT_NO_OF_GROUPS_ID); noOfGroupsField.addValidator(new GroupNumberValidator()); + noOfGroupsField.addValidator(new GroupSizeValidator()); noOfGroupsField.setMaxLength(3); noOfGroupsField.addValueChangeListener(this::onGroupNumberChange); return noOfGroupsField; @@ -850,8 +856,8 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { return rolloutNameField; } - class ErrorThresoldOptionValidator implements Validator { - private static final long serialVersionUID = 9049939751976326550L; + class ErrorThresholdOptionValidator implements Validator { + private static final long serialVersionUID = 1L; @Override public void validate(final Object value) { @@ -879,6 +885,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } class TargetExistsValidator implements Validator { + private static final long serialVersionUID = 1L; @Override @@ -891,7 +898,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } class ThresholdFieldValidator implements Validator { - private static final long serialVersionUID = 9049939751976326550L; + private static final long serialVersionUID = 1L; @Override public void validate(final Object value) { @@ -903,13 +910,29 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } class GroupNumberValidator implements Validator { - private static final long serialVersionUID = 9043919751971326521L; + private static final long serialVersionUID = 1L; @Override public void validate(final Object value) { if (value != null) { - new IntegerRangeValidator(i18n.getMessage(MESSAGE_ROLLOUT_FIELD_VALUE_RANGE, 1, 500), 1, 500) - .validate(Integer.valueOf(value.toString())); + final int maxGroups = quotaManagement.getMaxRolloutGroupsPerRollout(); + new IntegerRangeValidator(i18n.getMessage(MESSAGE_ROLLOUT_FIELD_VALUE_RANGE, 1, maxGroups), 1, + maxGroups).validate(Integer.valueOf(value.toString())); + } + } + } + + class GroupSizeValidator implements Validator { + private static final long serialVersionUID = 0L; + + @Override + public void validate(final Object value) { + if (value != null && totalTargetsCount != null) { + final int maxGroupSize = quotaManagement.getMaxTargetsPerRolloutGroup(); + if (getGroupSize() > maxGroupSize) { + final String msg = i18n.getMessage(MESSAGE_ROLLOUT_MAX_GROUP_SIZE_EXCEEDED, maxGroupSize); + throw new InvalidValueException(msg); + } } } } @@ -1020,12 +1043,15 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } } - private enum ERRORTHRESOLDOPTIONS { - PERCENT("%"), COUNT("Count"); + private enum ERROR_THRESHOLD_OPTIONS { - String value; + PERCENT("%"), - ERRORTHRESOLDOPTIONS(final String val) { + COUNT("Count"); + + private final String value; + + ERROR_THRESHOLD_OPTIONS(final String val) { value = val; } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/DefineGroupsLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/DefineGroupsLayout.java index bebe79ffd..8c8d29274 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/DefineGroupsLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/DefineGroupsLayout.java @@ -67,7 +67,9 @@ import com.vaadin.ui.UI; */ public class DefineGroupsLayout extends GridLayout { - private static final long serialVersionUID = 2939193468001472916L; + private static final long serialVersionUID = 1L; + + private static final String MESSAGE_ROLLOUT_MAX_GROUP_SIZE_EXCEEDED = "message.rollout.max.group.size.exceeded.advanced"; private final VaadinMessageSource i18n; @@ -267,7 +269,7 @@ public class DefineGroupsLayout extends GridLayout { validateRemainingTargets(); } else { - resetRemainingTargetsError(); + resetErrors(); setValidationStatus(ValidationStatus.INVALID); } } @@ -279,12 +281,12 @@ public class DefineGroupsLayout extends GridLayout { } } - private void resetRemainingTargetsError() { - groupRows.forEach(GroupRow::hideLastGroupError); + private void resetErrors() { + groupRows.forEach(GroupRow::resetError); } private void validateRemainingTargets() { - resetRemainingTargetsError(); + resetErrors(); if (targetFilter == null) { return; } @@ -318,16 +320,33 @@ public class DefineGroupsLayout extends GridLayout { } groupsValidation = validation; - final GroupRow lastRow = groupRows.get(groupRows.size() - 1); + final int lastIdx = groupRows.size() - 1; + final GroupRow lastRow = groupRows.get(lastIdx); if (groupsValidation != null && groupsValidation.isValid() && validationStatus != ValidationStatus.INVALID) { - lastRow.hideLastGroupError(); + lastRow.resetError(); setValidationStatus(ValidationStatus.VALID); - } else { - lastRow.markWithLastGroupError(); + lastRow.setError(i18n.getMessage("message.rollout.remaining.targets.error")); setValidationStatus(ValidationStatus.INVALID); } + // validate the single groups + final int maxTargets = quotaManagement.getMaxTargetsPerRolloutGroup(); + final boolean hasRemainingTargetsError = validationStatus == ValidationStatus.INVALID; + for (int i = 0; i < groupRows.size(); ++i) { + final GroupRow row = groupRows.get(i); + // do not mask the 'remaining targets' error + if (hasRemainingTargetsError && row.equals(lastRow)) { + continue; + } + row.resetError(); + final Long count = groupsValidation.getTargetsPerGroup().get(i); + if (count != null && count > maxTargets) { + row.setError(i18n.getMessage(MESSAGE_ROLLOUT_MAX_GROUP_SIZE_EXCEEDED, maxTargets)); + setValidationStatus(ValidationStatus.INVALID); + } + } + } private List getGroupsFromRows() { @@ -389,13 +408,9 @@ public class DefineGroupsLayout extends GridLayout { groupName = createTextField("textfield.name", UIComponentIdProvider.ROLLOUT_GROUP_LIST_GRID_ID); groupName.setValue(i18n.getMessage("textfield.rollout.group.default.name", groupsCount)); groupName.setStyleName("rollout-group-name"); - groupName.addValueChangeListener( + groupName.addValueChangeListener(event -> valueChanged()); - event -> valueChanged()); - - targetFilterQueryCombo = - - createTargetFilterQueryCombo(); + targetFilterQueryCombo = createTargetFilterQueryCombo(); populateTargetFilterQuery(); targetFilterQueryCombo.addValueChangeListener(event -> valueChanged()); @@ -617,18 +632,16 @@ public class DefineGroupsLayout extends GridLayout { && triggerThreshold.isValid() && errorThreshold.isValid(); } - private void markWithLastGroupError() { - targetPercentage - .setComponentError(new UserError(i18n.getMessage("message.rollout.remaining.targets.error"))); + private void setError(final String error) { + targetPercentage.setComponentError(new UserError(error)); } /** * Hides an error of the row */ - private void hideLastGroupError() { + private void resetError() { targetPercentage.setComponentError(null); } - } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIDefinitions.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIDefinitions.java index 823285b60..62e871c51 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIDefinitions.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIDefinitions.java @@ -18,6 +18,7 @@ import org.springframework.data.domain.Sort.Direction; * Class to provide the unchanged constants. */ public final class SPUIDefinitions { + /** * Available locales. */ @@ -27,10 +28,12 @@ public final class SPUIDefinitions { * Default locale. */ public static final String DEFAULT_LOCALE = "en"; + /** * Locale cookie name. */ public static final String COOKIE_NAME = "BOSCHSI_UII_LOCALE"; + /** * Lazy query container page size. */ @@ -166,10 +169,12 @@ public final class SPUIDefinitions { * New Software Module desc field id. */ public static final String NEW_SOFTWARE_TYPE_DESC = "software.type.add.desc"; + /** * Hide filter by dist type layout button. */ public static final String HIDE_FILTER_DIST_TYPE = "hide.filter.dist.type.layout"; + /** * New Distribution Type distribution field id. */ @@ -184,18 +189,22 @@ public final class SPUIDefinitions { * New Software Module key field id. */ public static final String NEW_SOFTWARE_TYPE_KEY = "software.type.add.key"; + /** * New Target tag desc field id. */ public static final String NEW_TARGET_TAG_DESC = "target.tag.add.desc"; + /** * New distribution Type set tag add icon id. */ public static final String ADD_DISTRIBUTION_TYPE_TAG = "distribution.type.tag.add"; + /** * New software module set type add icon id. */ public static final String ADD_SOFTWARE_MODULE_TYPE = "softwaremodule.type.add"; + /** * No data. */ @@ -220,10 +229,12 @@ public final class SPUIDefinitions { * Filter by tag key. */ public static final String FILTER_BY_TAG = "FilterByTag"; + /** * Filter by no tag button. */ public static final String FILTER_BY_NO_TAG = "FilterByNoTag"; + /** * Filter by target filter query. */ @@ -233,34 +244,42 @@ public final class SPUIDefinitions { * Filter by distribution key. */ public static final String FILTER_BY_DISTRIBUTION = "FilterByDistribution"; + /** * Filter by distributionSet Type. */ public static final String FILTER_BY_DISTRIBUTION_SET_TYPE = "FilterByDistributionSetType"; + /** * Order by pinnedTarget. */ public static final String ORDER_BY_PINNED_TARGET = "OrderByPinnedTarget"; + /** * Filter by text key. */ public static final String FILTER_BY_TEXT = "FilterByText"; + /** * Text field style. */ public static final String TEXT_STYLE = "text-style"; + /** * Show actions for a target. */ public static final String ACTIONS_BY_TARGET = "ActionsByTarget"; + /** * Show action-states for a action. */ public static final String ACTIONSTATES_BY_ACTION = "ActionStatesByAction"; + /** * Show messages for a action-status. */ public static final String MESSAGES_BY_ACTIONSTATUS = "MessagesByActionStatus"; + /** * Key for no-message MessageProxy. */ @@ -290,22 +309,27 @@ public final class SPUIDefinitions { * Target and distribution column width in save popup window. */ public static final float TARGET_DISTRIBUTION_COLUMN_WIDTH = 2.8F; + /** * Discard column width in save window popup. */ public static final float DISCARD_COLUMN_WIDTH = 1.2F; + /** * Target tag name. */ public static final String TAG_NAME = "target-tag-name"; + /** * Target tag desc. */ public static final String TAG_DESC = "target-tag-desc"; + /** * Software type name. */ public static final String TYPE_NAME = "type-name"; + /** * DistributionSet type name. */ @@ -320,6 +344,7 @@ public final class SPUIDefinitions { * DistributionSet type key. */ public static final String DIST_SET_TYPE_KEY = "dist-set-type-key"; + /** * Software type desc. */ @@ -334,6 +359,7 @@ public final class SPUIDefinitions { * Tag combo style. */ public static final String FILTER_TYPE_COMBO_STYLE = "filter-combo-specific-style"; + /** * Color label style. */ @@ -351,6 +377,7 @@ public final class SPUIDefinitions { * Target tag button id prefix. */ public static final String TARGET_TAG_ID_PREFIXS = "target.tag."; + /** * Distribution tag button id prefix. */ @@ -359,7 +386,7 @@ public final class SPUIDefinitions { /** * Space. */ - static final String SPACE = " "; + public static final String SPACE = " "; /** * Distribution tag button id prefix. @@ -370,27 +397,33 @@ public final class SPUIDefinitions { * DistributionSet Type tag button id prefix. */ public static final String DISTRIBUTION_SET_TYPE_ID_PREFIXS = "dist.set.type."; + /** * Target last query date format . */ public static final String LAST_QUERY_DATE_FORMAT = "EEE MMM d HH:mm:ss z yyyy"; + /** * Target last query date format . */ public static final String LAST_QUERY_DATE_FORMAT_SHORT = "MMM d HH:mm z ''yy"; + /** * Item Id used in drop comparisons. */ public static final String ITEMID = "itemId"; + /** * Expand action history. */ public static final String EXPAND_ACTION_HISTORY = "expand.action.history"; + /** * Filter by distribution key. */ public static final String ORDER_BY_DISTRIBUTION = "OrderByDistribution"; + /** Artifact upload related entries - start **/ /** * Artifact details by Base software module id. @@ -408,15 +441,15 @@ public final class SPUIDefinitions { * automatically and also horizontal scroll bars get displayed. Used for * Responsive UI. */ - static final int REQ_MIN_UPLOAD_BROWSER_WIDTH = 1250; + public static final int REQ_MIN_UPLOAD_BROWSER_WIDTH = 1250; public static final int MIN_UPLOAD_CONFIRMATION_POPUP_WIDTH = 1000; public static final int MIN_UPLOAD_CONFIRMATION_POPUP_HEIGHT = 310; - static final int MAX_UPLOAD_CONFIRMATION_POPUP_WIDTH = 1050; + public static final int MAX_UPLOAD_CONFIRMATION_POPUP_WIDTH = 1050; - static final int MAX_UPLOAD_CONFIRMATION_POPUP_HEIGHT = 360; + public static final int MAX_UPLOAD_CONFIRMATION_POPUP_HEIGHT = 360; /** Artifact upload related entries - end. **/ diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index 8d45a8efc..fe853009a 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -566,6 +566,8 @@ message.correct.invalid.value = Please correct invalid values message.enter.number = Please enter number message.rollout.field.value.range = Value should be in range {0} to {1} message.rollout.filter.target.exists = The selected target filter does not match any existing target +message.rollout.max.group.size.exceeded = The maximum group size of {0} targets is exceeded. Please increase the number of groups or select a different target filter +message.rollout.max.group.size.exceeded.advanced = The maximum size of {0} targets is exceeded for this group. Please re-distribute the targets across the groups and create further groups if needed message.rollout.started = Rollout {0} started successfully message.rollout.paused = Rollout {0} paused successfully message.rollout.resumed = Rollout {0} resumed successfully