finalize PrivateCloud, Cloud- and ManagedServer and ManagedWebspace Billingtems and HostingAssets (#63)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: #63 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
parent
04d9b43301
commit
d157730de7
@ -8,7 +8,11 @@ import static java.lang.String.join;
|
|||||||
public class MultiValidationException extends ValidationException {
|
public class MultiValidationException extends ValidationException {
|
||||||
|
|
||||||
private MultiValidationException(final List<String> violations) {
|
private MultiValidationException(final List<String> violations) {
|
||||||
super("[\n" + join(",\n", violations) + "\n]");
|
super(
|
||||||
|
violations.size() > 1
|
||||||
|
? "[\n" + join(",\n", violations) + "\n]"
|
||||||
|
: "[" + join(",\n", violations) + "]"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void throwInvalid(final List<String> violations) {
|
public static void throwInvalid(final List<String> violations) {
|
||||||
|
@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.booking.item.validators;
|
|||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
|
||||||
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
|
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
|
||||||
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
|
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
|
||||||
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -59,19 +60,24 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
|
|||||||
final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())
|
final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())
|
||||||
.stream()
|
.stream()
|
||||||
.map(subItem -> propDef.getValue(subItem.getResources()))
|
.map(subItem -> propDef.getValue(subItem.getResources()))
|
||||||
.map(HsBookingItemEntityValidator::toNonNullInteger)
|
.map(HsBookingItemEntityValidator::convertBooleanToInteger)
|
||||||
|
.map(HsBookingItemEntityValidator::toIntegerWithDefault0)
|
||||||
.reduce(0, Integer::sum);
|
.reduce(0, Integer::sum);
|
||||||
final var maxValue = getNonNullIntegerValue(propDef, bookingItem.getResources());
|
final var maxValue = getIntegerValueWithDefault0(propDef, bookingItem.getResources());
|
||||||
if (propDef.thresholdPercentage() != null ) {
|
if (propDef.thresholdPercentage() != null ) {
|
||||||
return totalValue > (maxValue * propDef.thresholdPercentage() / 100)
|
return totalValue > (maxValue * propDef.thresholdPercentage() / 100)
|
||||||
? "%s' maximum total is %d%s, but actual total %s %d%s, which exceeds threshold of %d%%"
|
? "%s' maximum total is %d%s, but actual total %s is %d%s, which exceeds threshold of %d%%"
|
||||||
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage())
|
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage())
|
||||||
: null;
|
: null;
|
||||||
} else {
|
} else {
|
||||||
return totalValue > maxValue
|
return totalValue > maxValue
|
||||||
? "%s' maximum total is %d%s, but actual total %s %d%s"
|
? "%s' maximum total is %d%s, but actual total %s is %d%s"
|
||||||
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit)
|
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Object convertBooleanToInteger(final Object value) {
|
||||||
|
return value instanceof Boolean ? BooleanUtils.toInteger((Boolean)value) : value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
|
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
|
||||||
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
||||||
|
|
||||||
@ -9,12 +8,21 @@ class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator {
|
|||||||
|
|
||||||
HsCloudServerBookingItemValidator() {
|
HsCloudServerBookingItemValidator() {
|
||||||
super(
|
super(
|
||||||
|
// @formatter:off
|
||||||
|
booleanProperty("active") .withDefault(true),
|
||||||
|
|
||||||
integerProperty("CPUs") .min( 1) .max( 32) .required(),
|
integerProperty("CPUs") .min( 1) .max( 32) .required(),
|
||||||
integerProperty("RAM").unit("GB") .min( 1) .max( 128) .required(),
|
integerProperty("RAM").unit("GB") .min( 1) .max( 128) .required(),
|
||||||
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
|
integerProperty("SSD").unit("GB") .min( 0) .max( 1000) .step(25).required(), // (1)
|
||||||
integerProperty("HDD").unit("GB") .min( 0) .max( 4000) .step(250).withDefault(0),
|
integerProperty("HDD").unit("GB") .min( 0) .max( 4000) .step(250).withDefault(0),
|
||||||
integerProperty("Traffic").unit("GB") .min(250) .max(10000) .step(250).required(),
|
integerProperty("Traffic").unit("GB") .min(250) .max(10000) .step(250).required(),
|
||||||
|
|
||||||
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
|
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
|
||||||
|
// @formatter:on
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// (q) We do have pre-existing CloudServers without SSD, just HDD, thus SSD starts with min=0.
|
||||||
|
// TODO.impl: Validation that SSD+HDD is at minimum 25 GB is missing.
|
||||||
|
// e.g. validationGroup("SSD", "HDD").min(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,40 @@
|
|||||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
|
|
||||||
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
||||||
|
|
||||||
class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator {
|
class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator {
|
||||||
|
|
||||||
HsPrivateCloudBookingItemValidator() {
|
HsPrivateCloudBookingItemValidator() {
|
||||||
super(
|
super(
|
||||||
integerProperty("CPUs").min(4).max(128).required().asTotalLimit(),
|
// @formatter:off
|
||||||
integerProperty("RAM").unit("GB").min(4).max(512).required().asTotalLimit(),
|
integerProperty("CPUs") .min( 1).max( 128).required().asTotalLimit(),
|
||||||
integerProperty("SSD").unit("GB").min(100).max(4000).step(25).required().asTotalLimit(),
|
integerProperty("RAM").unit("GB") .min( 1).max( 512).required().asTotalLimit(),
|
||||||
integerProperty("HDD").unit("GB").min(0).max(16000).step(25).withDefault(0).asTotalLimit(),
|
integerProperty("SSD").unit("GB") .min( 25).max( 4000).step(25).required().asTotalLimit(),
|
||||||
integerProperty("Traffic").unit("GB").min(1000).max(40000).step(250).required().asTotalLimit(),
|
integerProperty("HDD").unit("GB") .min( 0).max(16000).step(250).withDefault(0).asTotalLimit(),
|
||||||
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC")
|
integerProperty("Traffic").unit("GB") .min(250).max(40000).step(250).required().asTotalLimit(),
|
||||||
|
|
||||||
|
// Alternatively we could specify it similarly to "Multi" option but exclusively counting:
|
||||||
|
// integerProperty("Resource-Points") .min(4).max(100).required()
|
||||||
|
// .each("CPUs").countsAs(64)
|
||||||
|
// .each("RAM").countsAs(64)
|
||||||
|
// .each("SSD").countsAs(18)
|
||||||
|
// .each("HDD").countsAs(2)
|
||||||
|
// .each("Traffic").countsAs(1),
|
||||||
|
|
||||||
|
integerProperty("SLA-Infrastructure EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT8H"),
|
||||||
|
integerProperty("SLA-Infrastructure EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT4H"),
|
||||||
|
integerProperty("SLA-Infrastructure EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT2H"),
|
||||||
|
|
||||||
|
integerProperty("SLA-Platform EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT8H"),
|
||||||
|
integerProperty("SLA-Platform EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT4H"),
|
||||||
|
integerProperty("SLA-Platform EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT2H"),
|
||||||
|
|
||||||
|
integerProperty("SLA-EMail") .min( 0).max( 20).withDefault(0).asTotalLimit(),
|
||||||
|
integerProperty("SLA-Maria") .min( 0).max( 20).withDefault(0).asTotalLimit(),
|
||||||
|
integerProperty("SLA-PgSQL") .min( 0).max( 20).withDefault(0).asTotalLimit(),
|
||||||
|
integerProperty("SLA-Office") .min( 0).max( 20).withDefault(0).asTotalLimit(),
|
||||||
|
integerProperty("SLA-Web") .min( 0).max( 20).withDefault(0).asTotalLimit()
|
||||||
|
// @formatter:on
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
|
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
@ -34,6 +35,9 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private HsHostingAssetRepository assetRepo;
|
private HsHostingAssetRepository assetRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private HsBookingItemRepository bookingItemRepo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public ResponseEntity<List<HsHostingAssetResource>> listAssets(
|
public ResponseEntity<List<HsHostingAssetResource>> listAssets(
|
||||||
@ -124,6 +128,11 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
|
|
||||||
final BiConsumer<HsHostingAssetInsertResource, HsHostingAssetEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
final BiConsumer<HsHostingAssetInsertResource, HsHostingAssetEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||||
entity.putConfig(KeyValueMap.from(resource.getConfig()));
|
entity.putConfig(KeyValueMap.from(resource.getConfig()));
|
||||||
|
if (resource.getBookingItemUuid() != null) {
|
||||||
|
entity.setBookingItem(bookingItemRepo.findByUuid(resource.getBookingItemUuid())
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] bookingItemUuid %s not found".formatted(
|
||||||
|
resource.getBookingItemUuid()))));
|
||||||
|
}
|
||||||
if (resource.getParentAssetUuid() != null) {
|
if (resource.getParentAssetUuid() != null) {
|
||||||
entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid())
|
entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid())
|
||||||
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted(
|
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted(
|
||||||
|
@ -27,6 +27,7 @@ import jakarta.persistence.Id;
|
|||||||
import jakarta.persistence.JoinColumn;
|
import jakarta.persistence.JoinColumn;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.OneToMany;
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import jakarta.persistence.Transient;
|
import jakarta.persistence.Transient;
|
||||||
import jakarta.persistence.Version;
|
import jakarta.persistence.Version;
|
||||||
@ -78,7 +79,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
|
|||||||
@Version
|
@Version
|
||||||
private int version;
|
private int version;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "bookingitemuuid")
|
@JoinColumn(name = "bookingitemuuid")
|
||||||
private HsBookingItemEntity bookingItem;
|
private HsBookingItemEntity bookingItem;
|
||||||
|
|
||||||
@ -142,7 +143,6 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
|
|||||||
dependsOnColumn("bookingItemUuid"),
|
dependsOnColumn("bookingItemUuid"),
|
||||||
directlyFetchedByDependsOnColumn(),
|
directlyFetchedByDependsOnColumn(),
|
||||||
NULLABLE)
|
NULLABLE)
|
||||||
.toRole("bookingItem", AGENT).grantPermission(INSERT)
|
|
||||||
|
|
||||||
.importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(),
|
.importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(),
|
||||||
dependsOnColumn("parentAssetUuid"),
|
dependsOnColumn("parentAssetUuid"),
|
||||||
|
@ -65,11 +65,11 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAs
|
|||||||
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
|
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
|
||||||
.stream()
|
.stream()
|
||||||
.map(subItem -> propDef.getValue(subItem.getConfig()))
|
.map(subItem -> propDef.getValue(subItem.getConfig()))
|
||||||
.map(HsEntityValidator::toNonNullInteger)
|
.map(HsEntityValidator::toIntegerWithDefault0)
|
||||||
.reduce(0, Integer::sum);
|
.reduce(0, Integer::sum);
|
||||||
final var maxValue = getNonNullIntegerValue(propDef, hostingAsset.getConfig());
|
final var maxValue = getIntegerValueWithDefault0(propDef, hostingAsset.getConfig());
|
||||||
return totalValue > maxValue
|
return totalValue > maxValue
|
||||||
? "%s' maximum total is %d%s, but actual total is %s %d%s".formatted(
|
? "%s' maximum total is %d%s, but actual total %s is %d%s".formatted(
|
||||||
propName, maxValue, propUnit, propName, totalValue, propUnit)
|
propName, maxValue, propUnit, propName, totalValue, propUnit)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,48 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
|
||||||
|
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
|
||||||
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
||||||
|
|
||||||
class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator {
|
class HsManagedServerHostingAssetValidator extends HsHostingAssetEntityValidator {
|
||||||
|
|
||||||
public HsManagedServerHostingAssetValidator() {
|
public HsManagedServerHostingAssetValidator() {
|
||||||
super(
|
super(
|
||||||
integerProperty("monit_min_free_ssd").min(1).max(1000).optional(),
|
// monitoring
|
||||||
integerProperty("monit_min_free_hdd").min(1).max(4000).optional(),
|
integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92),
|
||||||
integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).required(),
|
integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).withDefault(92),
|
||||||
integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).optional(),
|
integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).withDefault(98),
|
||||||
integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).required(),
|
integerProperty("monit_min_free_ssd").min(1).max(1000).withDefault(5),
|
||||||
integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).required()
|
integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).withDefault(95),
|
||||||
// TODO: stringProperty("monit_alarm_email").unit("GB").optional()
|
integerProperty("monit_min_free_hdd").min(1).max(4000).withDefault(10),
|
||||||
|
// stringProperty("monit_alarm_email").unit("GB").optional() TODO.impl: via Contact?
|
||||||
|
|
||||||
|
// other settings
|
||||||
|
// booleanProperty("fastcgi_small").withDefault(false), TODO.spec: clarify Salt-Grains
|
||||||
|
|
||||||
|
// database software
|
||||||
|
booleanProperty("software-pgsql").withDefault(true),
|
||||||
|
booleanProperty("software-mariadb").withDefault(true),
|
||||||
|
|
||||||
|
// PHP
|
||||||
|
enumerationProperty("php-default").valuesFromProperties("software-php-").withDefault("8.2"),
|
||||||
|
booleanProperty("software-php-5.6").withDefault(false),
|
||||||
|
booleanProperty("software-php-7.0").withDefault(false),
|
||||||
|
booleanProperty("software-php-7.1").withDefault(false),
|
||||||
|
booleanProperty("software-php-7.2").withDefault(false),
|
||||||
|
booleanProperty("software-php-7.3").withDefault(false),
|
||||||
|
booleanProperty("software-php-7.4").withDefault(true),
|
||||||
|
booleanProperty("software-php-8.0").withDefault(false),
|
||||||
|
booleanProperty("software-php-8.1").withDefault(false),
|
||||||
|
booleanProperty("software-php-8.2").withDefault(true),
|
||||||
|
|
||||||
|
// other software
|
||||||
|
booleanProperty("software-postfix-tls-1.0").withDefault(false),
|
||||||
|
booleanProperty("software-dovecot-tls-1.0").withDefault(false),
|
||||||
|
booleanProperty("software-clamav").withDefault(true),
|
||||||
|
booleanProperty("software-collabora").withDefault(false),
|
||||||
|
booleanProperty("software-libreoffice").withDefault(false),
|
||||||
|
booleanProperty("software-imagemagick-ghostscript").withDefault(false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.util.Arrays.stream;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
public class EnumerationProperty extends ValidatableProperty<String> {
|
public class EnumerationProperty extends ValidatableProperty<String> {
|
||||||
|
|
||||||
@ -30,9 +32,27 @@ public class EnumerationProperty extends ValidatableProperty<String> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
|
||||||
|
if (deferredInit != null) {
|
||||||
|
if (this.values != null) {
|
||||||
|
throw new IllegalStateException("property " + toString() + " already has values");
|
||||||
|
}
|
||||||
|
this.values = deferredInit.apply(allProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidatableProperty<String> valuesFromProperties(final String propertyNamePrefix) {
|
||||||
|
this.deferredInit = (ValidatableProperty<?>[] allProperties) -> stream(allProperties)
|
||||||
|
.map(ValidatableProperty::propertyName)
|
||||||
|
.filter(name -> name.startsWith(propertyNamePrefix))
|
||||||
|
.map(name -> name.substring(propertyNamePrefix.length()))
|
||||||
|
.toArray(String[]::new);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
|
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
|
||||||
if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
|
if (stream(values).noneMatch(v -> v.equals(propValue))) {
|
||||||
result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
|
result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ public abstract class HsEntityValidator<E> {
|
|||||||
|
|
||||||
public HsEntityValidator(final ValidatableProperty<?>... validators) {
|
public HsEntityValidator(final ValidatableProperty<?>... validators) {
|
||||||
propertyValidators = validators;
|
propertyValidators = validators;
|
||||||
|
stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static List<String> enrich(final String prefix, final List<String> messages) {
|
protected static List<String> enrich(final String prefix, final List<String> messages) {
|
||||||
@ -59,18 +60,24 @@ public abstract class HsEntityValidator<E> {
|
|||||||
.orElse(emptyList()));
|
.orElse(emptyList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static Integer getNonNullIntegerValue(final ValidatableProperty<?> prop, final Map<String, Object> propValues) {
|
protected static Integer getIntegerValueWithDefault0(final ValidatableProperty<?> prop, final Map<String, Object> propValues) {
|
||||||
final var value = prop.getValue(propValues);
|
final var value = prop.getValue(propValues);
|
||||||
if (value instanceof Integer) {
|
if (value instanceof Integer) {
|
||||||
return (Integer) value;
|
return (Integer) value;
|
||||||
}
|
}
|
||||||
|
if (value == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value);
|
throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static Integer toNonNullInteger(final Object value) {
|
protected static Integer toIntegerWithDefault0(final Object value) {
|
||||||
if (value instanceof Integer) {
|
if (value instanceof Integer) {
|
||||||
return (Integer) value;
|
return (Integer) value;
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("Integer value expected, but got " + value);
|
if (value == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Integer value (or null) expected, but got " + value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import java.util.function.Function;
|
|||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public abstract class ValidatableProperty<T> {
|
public abstract class ValidatableProperty<T> {
|
||||||
@ -31,6 +32,7 @@ public abstract class ValidatableProperty<T> {
|
|||||||
private final String[] keyOrder;
|
private final String[] keyOrder;
|
||||||
private Boolean required;
|
private Boolean required;
|
||||||
private T defaultValue;
|
private T defaultValue;
|
||||||
|
protected Function<ValidatableProperty<?>[], T[]> deferredInit;
|
||||||
private boolean isTotalsValidator = false;
|
private boolean isTotalsValidator = false;
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private List<Function<HsBookingItemEntity, List<String>>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty
|
private List<Function<HsBookingItemEntity, List<String>>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty
|
||||||
@ -57,11 +59,38 @@ public abstract class ValidatableProperty<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
|
||||||
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> asTotalLimit() {
|
public ValidatableProperty<T> asTotalLimit() {
|
||||||
isTotalsValidator = true;
|
isTotalsValidator = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ValidatableProperty<T> asTotalLimitFor(final String propertyName, final String propertyValue) {
|
||||||
|
if (asTotalLimitValidators == null) {
|
||||||
|
asTotalLimitValidators = new ArrayList<>();
|
||||||
|
}
|
||||||
|
final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> validator =
|
||||||
|
(final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
|
||||||
|
|
||||||
|
final var total = entity.getSubBookingItems().stream()
|
||||||
|
.map(server -> server.getResources().get(propertyName))
|
||||||
|
.filter(propertyValue::equals)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
final long limitingValue = ofNullable(prop.getValue(entity.getResources())).orElse(0);
|
||||||
|
if (total > factor*limitingValue) {
|
||||||
|
return List.of(
|
||||||
|
prop.propertyName() + " maximum total is " + (factor*limitingValue) + ", but actual total for " + propertyName + "=" + propertyValue + " is " + total
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return emptyList();
|
||||||
|
};
|
||||||
|
asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public String propertyName() {
|
public String propertyName() {
|
||||||
return propertyName;
|
return propertyName;
|
||||||
}
|
}
|
||||||
|
@ -55,9 +55,10 @@ class HsCloudServerBookingItemValidatorUnitTest {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
|
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
|
||||||
|
"{type=boolean, propertyName=active, required=false, defaultValue=true, isTotalsValidator=false}",
|
||||||
"{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}",
|
"{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}",
|
||||||
"{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}",
|
"{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}",
|
||||||
"{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=false}",
|
"{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, required=true, isTotalsValidator=false}",
|
||||||
"{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}",
|
"{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}",
|
||||||
"{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}",
|
"{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}",
|
||||||
"{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}");
|
"{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}");
|
||||||
@ -109,10 +110,10 @@ class HsCloudServerBookingItemValidatorUnitTest {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(result).containsExactlyInAnyOrder(
|
assertThat(result).containsExactlyInAnyOrder(
|
||||||
"'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs 5",
|
"'D-12345:Test-Project:Test Cloud.resources.CPUs' maximum total is 4, but actual total CPUs is 5",
|
||||||
"'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
|
"'D-12345:Test-Project:Test Cloud.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
|
||||||
"'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
|
"'D-12345:Test-Project:Test Cloud.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
|
||||||
"'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
|
"'D-12345:Test-Project:Test Cloud.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,10 +120,10 @@ class HsManagedServerBookingItemValidatorUnitTest {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(result).containsExactlyInAnyOrder(
|
assertThat(result).containsExactlyInAnyOrder(
|
||||||
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5",
|
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs is 5",
|
||||||
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
|
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
|
||||||
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
|
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
|
||||||
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
|
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,29 +28,38 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
|
|||||||
// given
|
// given
|
||||||
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
|
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
|
||||||
.type(PRIVATE_CLOUD)
|
.type(PRIVATE_CLOUD)
|
||||||
|
.caption("myPC")
|
||||||
.resources(ofEntries(
|
.resources(ofEntries(
|
||||||
entry("CPUs", 4),
|
entry("CPUs", 4),
|
||||||
entry("RAM", 20),
|
entry("RAM", 20),
|
||||||
entry("SSD", 100),
|
entry("SSD", 100),
|
||||||
entry("Traffic", 5000)
|
entry("Traffic", 5000),
|
||||||
|
entry("SLA-Platform EXT4H", 2),
|
||||||
|
entry("SLA-EMail", 2)
|
||||||
))
|
))
|
||||||
.subBookingItems(of(
|
.subBookingItems(of(
|
||||||
HsBookingItemEntity.builder()
|
HsBookingItemEntity.builder()
|
||||||
.type(MANAGED_SERVER)
|
.type(MANAGED_SERVER)
|
||||||
|
.caption("myMS-1")
|
||||||
.resources(ofEntries(
|
.resources(ofEntries(
|
||||||
entry("CPUs", 2),
|
entry("CPUs", 2),
|
||||||
entry("RAM", 10),
|
entry("RAM", 10),
|
||||||
entry("SSD", 50),
|
entry("SSD", 50),
|
||||||
entry("Traffic", 2500)
|
entry("Traffic", 2500),
|
||||||
|
entry("SLA-Platform", "EXT4H"),
|
||||||
|
entry("SLA-EMail", true)
|
||||||
))
|
))
|
||||||
.build(),
|
.build(),
|
||||||
HsBookingItemEntity.builder()
|
HsBookingItemEntity.builder()
|
||||||
.type(CLOUD_SERVER)
|
.type(CLOUD_SERVER)
|
||||||
|
.caption("myMS-2")
|
||||||
.resources(ofEntries(
|
.resources(ofEntries(
|
||||||
entry("CPUs", 2),
|
entry("CPUs", 2),
|
||||||
entry("RAM", 10),
|
entry("RAM", 10),
|
||||||
entry("SSD", 50),
|
entry("SSD", 50),
|
||||||
entry("Traffic", 2500)
|
entry("Traffic", 2500),
|
||||||
|
entry("SLA-Platform", "EXT4H"),
|
||||||
|
entry("SLA-EMail", true)
|
||||||
))
|
))
|
||||||
.build()
|
.build()
|
||||||
))
|
))
|
||||||
@ -69,29 +78,42 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
|
|||||||
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
|
final var privateCloudBookingItemEntity = HsBookingItemEntity.builder()
|
||||||
.project(project)
|
.project(project)
|
||||||
.type(PRIVATE_CLOUD)
|
.type(PRIVATE_CLOUD)
|
||||||
|
.caption("myPC")
|
||||||
.resources(ofEntries(
|
.resources(ofEntries(
|
||||||
entry("CPUs", 4),
|
entry("CPUs", 4),
|
||||||
entry("RAM", 20),
|
entry("RAM", 20),
|
||||||
entry("SSD", 100),
|
entry("SSD", 100),
|
||||||
entry("Traffic", 5000)
|
entry("Traffic", 5000),
|
||||||
|
entry("SLA-Platform EXT2H", 1),
|
||||||
|
entry("SLA-EMail", 1)
|
||||||
))
|
))
|
||||||
.subBookingItems(of(
|
.subBookingItems(of(
|
||||||
HsBookingItemEntity.builder()
|
HsBookingItemEntity.builder()
|
||||||
.type(MANAGED_SERVER)
|
.type(MANAGED_SERVER)
|
||||||
|
.caption("myMS-1")
|
||||||
.resources(ofEntries(
|
.resources(ofEntries(
|
||||||
entry("CPUs", 3),
|
entry("CPUs", 3),
|
||||||
entry("RAM", 20),
|
entry("RAM", 20),
|
||||||
entry("SSD", 100),
|
entry("SSD", 100),
|
||||||
entry("Traffic", 3000)
|
entry("Traffic", 3000),
|
||||||
|
entry("SLA-Platform", "EXT2H"),
|
||||||
|
entry("SLA-EMail", true)
|
||||||
))
|
))
|
||||||
.build(),
|
.build(),
|
||||||
HsBookingItemEntity.builder()
|
HsBookingItemEntity.builder()
|
||||||
.type(CLOUD_SERVER)
|
.type(CLOUD_SERVER)
|
||||||
|
.caption("myMS-2")
|
||||||
.resources(ofEntries(
|
.resources(ofEntries(
|
||||||
entry("CPUs", 2),
|
entry("CPUs", 2),
|
||||||
entry("RAM", 10),
|
entry("RAM", 10),
|
||||||
entry("SSD", 50),
|
entry("SSD", 50),
|
||||||
entry("Traffic", 2500)
|
entry("Traffic", 2500),
|
||||||
|
entry("SLA-Platform", "EXT2H"),
|
||||||
|
entry("SLA-EMail", true),
|
||||||
|
entry("SLA-Maria", true),
|
||||||
|
entry("SLA-PgSQL", true),
|
||||||
|
entry("SLA-Office", true),
|
||||||
|
entry("SLA-Web", true)
|
||||||
))
|
))
|
||||||
.build()
|
.build()
|
||||||
))
|
))
|
||||||
@ -102,11 +124,16 @@ class HsPrivateCloudBookingItemValidatorUnitTest {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(result).containsExactlyInAnyOrder(
|
assertThat(result).containsExactlyInAnyOrder(
|
||||||
"'D-12345:Test-Project:null.resources.CPUs' maximum total is 4, but actual total CPUs 5",
|
"'D-12345:Test-Project:myPC.resources.CPUs' maximum total is 4, but actual total CPUs is 5",
|
||||||
"'D-12345:Test-Project:null.resources.RAM' maximum total is 20 GB, but actual total RAM 30 GB",
|
"'D-12345:Test-Project:myPC.resources.RAM' maximum total is 20 GB, but actual total RAM is 30 GB",
|
||||||
"'D-12345:Test-Project:null.resources.SSD' maximum total is 100 GB, but actual total SSD 150 GB",
|
"'D-12345:Test-Project:myPC.resources.SSD' maximum total is 100 GB, but actual total SSD is 150 GB",
|
||||||
"'D-12345:Test-Project:null.resources.Traffic' maximum total is 5000 GB, but actual total Traffic 5500 GB"
|
"'D-12345:Test-Project:myPC.resources.Traffic' maximum total is 5000 GB, but actual total Traffic is 5500 GB",
|
||||||
|
"'D-12345:Test-Project:myPC.resources.SLA-Platform EXT2H maximum total is 1, but actual total for SLA-Platform=EXT2H is 2",
|
||||||
|
"'D-12345:Test-Project:myPC.resources.SLA-EMail' maximum total is 1, but actual total SLA-EMail is 2",
|
||||||
|
"'D-12345:Test-Project:myPC.resources.SLA-Maria' maximum total is 0, but actual total SLA-Maria is 1",
|
||||||
|
"'D-12345:Test-Project:myPC.resources.SLA-PgSQL' maximum total is 0, but actual total SLA-PgSQL is 1",
|
||||||
|
"'D-12345:Test-Project:myPC.resources.SLA-Office' maximum total is 0, but actual total SLA-Office is 1",
|
||||||
|
"'D-12345:Test-Project:myPC.resources.SLA-Web' maximum total is 0, but actual total SLA-Web is 1"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,11 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRepository;
|
|||||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
|
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorRepository;
|
||||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
|
import org.junit.jupiter.api.ClassOrderer;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestClassOrder;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||||
@ -28,11 +31,12 @@ import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.hamcrest.Matchers.matchesRegex;
|
import static org.hamcrest.Matchers.matchesRegex;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@SpringBootTest(
|
@SpringBootTest(
|
||||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||||
classes = { HsadminNgApplication.class, JpaAttempt.class }
|
classes = { HsadminNgApplication.class, JpaAttempt.class }
|
||||||
)
|
)
|
||||||
@Transactional
|
@TestClassOrder(ClassOrderer.OrderAnnotation.class) // fail early on fetching problems
|
||||||
class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
||||||
|
|
||||||
@LocalServerPort
|
@LocalServerPort
|
||||||
@ -54,6 +58,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
JpaAttempt jpaAttempt;
|
JpaAttempt jpaAttempt;
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
@Order(2)
|
||||||
class ListAssets {
|
class ListAssets {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -152,6 +157,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
@Order(3)
|
||||||
class AddAsset {
|
class AddAsset {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -280,9 +286,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"statusPhrase": "Bad Request",
|
"statusPhrase": "Bad Request",
|
||||||
"message": "[
|
"message": "[
|
||||||
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
|
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
|
||||||
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0,
|
|
||||||
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
|
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
|
||||||
<<<'MANAGED_SERVER:vm1400.config.monit_max_ram_usage' is required but missing
|
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0
|
||||||
<<<]"
|
<<<]"
|
||||||
}
|
}
|
||||||
""".replaceAll(" +<<<", ""))); // @formatter:on
|
""".replaceAll(" +<<<", ""))); // @formatter:on
|
||||||
@ -333,15 +338,14 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
.body("", lenientlyEquals("""
|
.body("", lenientlyEquals("""
|
||||||
{
|
{
|
||||||
"statusPhrase": "Bad Request",
|
"statusPhrase": "Bad Request",
|
||||||
"message": "[
|
"message": "['D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found]"
|
||||||
<<<'D-1000111:D-1000111 default project:separate ManagedWebspace.resources.Multi=1 allows at maximum 25 unix users, but 26 found
|
|
||||||
<<<]"
|
|
||||||
}
|
}
|
||||||
""".replaceAll(" +<<<", ""))); // @formatter:on
|
""".replaceAll(" +<<<", ""))); // @formatter:on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
@Order(1)
|
||||||
class GetAsset {
|
class GetAsset {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -413,6 +417,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
@Order(4)
|
||||||
class PatchAsset {
|
class PatchAsset {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -466,6 +471,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
@Order(5)
|
||||||
class DeleteAsset {
|
class DeleteAsset {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -55,18 +55,22 @@ class HsHostingAssetPropsControllerAcceptanceTest {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"propertyName": "monit_min_free_ssd",
|
"propertyName": "monit_max_cpu_usage",
|
||||||
"min": 1,
|
"unit": "%",
|
||||||
"max": 1000,
|
"min": 10,
|
||||||
|
"max": 100,
|
||||||
"required": false,
|
"required": false,
|
||||||
|
"defaultValue": 92,
|
||||||
"isTotalsValidator": false
|
"isTotalsValidator": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"propertyName": "monit_min_free_hdd",
|
"propertyName": "monit_max_ram_usage",
|
||||||
"min": 1,
|
"unit": "%",
|
||||||
"max": 4000,
|
"min": 10,
|
||||||
|
"max": 100,
|
||||||
"required": false,
|
"required": false,
|
||||||
|
"defaultValue": 92,
|
||||||
"isTotalsValidator": false
|
"isTotalsValidator": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -75,7 +79,17 @@ class HsHostingAssetPropsControllerAcceptanceTest {
|
|||||||
"unit": "%",
|
"unit": "%",
|
||||||
"min": 10,
|
"min": 10,
|
||||||
"max": 100,
|
"max": 100,
|
||||||
"required": true,
|
"required": false,
|
||||||
|
"defaultValue": 98,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"propertyName": "monit_min_free_ssd",
|
||||||
|
"min": 1,
|
||||||
|
"max": 1000,
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": 5,
|
||||||
"isTotalsValidator": false
|
"isTotalsValidator": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -85,29 +99,157 @@ class HsHostingAssetPropsControllerAcceptanceTest {
|
|||||||
"min": 10,
|
"min": 10,
|
||||||
"max": 100,
|
"max": 100,
|
||||||
"required": false,
|
"required": false,
|
||||||
|
"defaultValue": 95,
|
||||||
"isTotalsValidator": false
|
"isTotalsValidator": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"propertyName": "monit_max_cpu_usage",
|
"propertyName": "monit_min_free_hdd",
|
||||||
"unit": "%",
|
"min": 1,
|
||||||
"min": 10,
|
"max": 4000,
|
||||||
"max": 100,
|
"required": false,
|
||||||
"required": true,
|
"defaultValue": 10,
|
||||||
"isTotalsValidator": false
|
"isTotalsValidator": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "boolean",
|
||||||
"propertyName": "monit_max_ram_usage",
|
"propertyName": "software-pgsql",
|
||||||
"unit": "%",
|
"required": false,
|
||||||
"min": 10,
|
"defaultValue": true,
|
||||||
"max": 100,
|
"isTotalsValidator": false
|
||||||
"required": true,
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-mariadb",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": true,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "enumeration",
|
||||||
|
"propertyName": "php-default",
|
||||||
|
"values": [
|
||||||
|
"5.6",
|
||||||
|
"7.0",
|
||||||
|
"7.1",
|
||||||
|
"7.2",
|
||||||
|
"7.3",
|
||||||
|
"7.4",
|
||||||
|
"8.0",
|
||||||
|
"8.1",
|
||||||
|
"8.2"
|
||||||
|
],
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": "8.2",
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-php-5.6",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-php-7.0",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-php-7.1",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-php-7.2",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-php-7.3",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-php-7.4",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": true,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-php-8.0",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-php-8.1",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-php-8.2",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": true,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-postfix-tls-1.0",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-dovecot-tls-1.0",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-clamav",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": true,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-collabora",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-libreoffice",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
|
"isTotalsValidator": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "software-imagemagick-ghostscript",
|
||||||
|
"required": false,
|
||||||
|
"defaultValue": false,
|
||||||
"isTotalsValidator": false
|
"isTotalsValidator": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
"""));
|
"""));
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
|||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import jakarta.validation.ValidationException;
|
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@ -23,10 +22,6 @@ class HsHostingAssetEntityValidatorUnitTest {
|
|||||||
final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity));
|
final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(result).isInstanceOf(ValidationException.class)
|
assertThat(result).isNull(); // all required properties have defaults
|
||||||
.hasMessageContaining(
|
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing",
|
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is required but missing",
|
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is required but missing");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,6 @@ class HsManagedServerHostingAssetValidatorUnitTest {
|
|||||||
assertThat(result).containsExactlyInAnyOrder(
|
assertThat(result).containsExactlyInAnyOrder(
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
|
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101",
|
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101",
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_ssd_usage' is required but missing",
|
|
||||||
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
|
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,9 +109,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
|||||||
assertThat(debitorRepo.count()).isEqualTo(count + 1);
|
assertThat(debitorRepo.count()).isEqualTo(count + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = {"", "a", "ab", "a12", "123", "12a"})
|
@ValueSource(strings = {"", "a", "ab", "a12", "123", "12a"})
|
||||||
@Transactional
|
|
||||||
public void canNotCreateNewDebitorWithInvalidDefaultPrefix(final String givenPrefix) {
|
public void canNotCreateNewDebitorWithInvalidDefaultPrefix(final String givenPrefix) {
|
||||||
// given
|
// given
|
||||||
context("superuser-alex@hostsharing.net");
|
context("superuser-alex@hostsharing.net");
|
||||||
|
@ -14,9 +14,12 @@ import org.junit.jupiter.api.TestInfo;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.Repository;
|
import org.springframework.data.repository.Repository;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import static java.lang.System.out;
|
import static java.lang.System.out;
|
||||||
import static java.util.Comparator.comparing;
|
import static java.util.Comparator.comparing;
|
||||||
@ -28,9 +31,13 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
||||||
|
|
||||||
private static final boolean DETAILED_BUT_SLOW_CHECK = true;
|
private static final boolean DETAILED_BUT_SLOW_CHECK = true;
|
||||||
|
|
||||||
@PersistenceContext
|
@PersistenceContext
|
||||||
protected EntityManager em;
|
protected EntityManager em;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PlatformTransactionManager tm;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
RbacGrantRepository rbacGrantRepo;
|
RbacGrantRepository rbacGrantRepo;
|
||||||
|
|
||||||
@ -166,13 +173,17 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void cleanupAndCheckCleanup(final TestInfo testInfo) {
|
void cleanupAndCheckCleanup(final TestInfo testInfo) {
|
||||||
|
// If the whole test method has its own transaction, cleanup makes no sense.
|
||||||
|
// If that transaction even failed, cleaunup would cause an exception.
|
||||||
|
if (!tm.getTransaction(null).isRollbackOnly()) {
|
||||||
out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup");
|
out.println(ContextBasedTestWithCleanup.class.getSimpleName() + ".cleanupAndCheckCleanup");
|
||||||
cleanupTemporaryTestData();
|
cleanupTemporaryTestData();
|
||||||
deleteLeakedRbacObjects();
|
repeatUntilTrue(3, this::deleteLeakedRbacObjects);
|
||||||
long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked();
|
|
||||||
|
|
||||||
|
long rbacObjectCount = assertNoNewRbacObjectsRolesAndGrantsLeaked();
|
||||||
out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount);
|
out.println("TOTAL OBJECT COUNT (after): " + rbacObjectCount);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void cleanupTemporaryTestData() {
|
private void cleanupTemporaryTestData() {
|
||||||
// For better performance in a single transaction ...
|
// For better performance in a single transaction ...
|
||||||
@ -218,7 +229,8 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
}).assertSuccessful().returnedValue();
|
}).assertSuccessful().returnedValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteLeakedRbacObjects() {
|
private boolean deleteLeakedRbacObjects() {
|
||||||
|
final var deletionSuccessful = new AtomicBoolean(true);
|
||||||
rbacObjectRepo.findAll().stream()
|
rbacObjectRepo.findAll().stream()
|
||||||
.filter(o -> o.serialId > latestIntialTestDataSerialId)
|
.filter(o -> o.serialId > latestIntialTestDataSerialId)
|
||||||
.sorted(comparing(o -> o.serialId))
|
.sorted(comparing(o -> o.serialId))
|
||||||
@ -235,8 +247,10 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
|
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception);
|
out.println("DELETING leaked " + o.objectTable + "#" + o.uuid + " FAILED " + exception);
|
||||||
|
deletionSuccessful.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return deletionSuccessful.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertEqual(final Set<String> before, final Set<String> after) {
|
private void assertEqual(final Set<String> before, final Set<String> after) {
|
||||||
@ -297,6 +311,15 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
"doc/temp/" + name + ".md"
|
"doc/temp/" + name + ".md"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean repeatUntilTrue(int maxAttempts, Supplier<Boolean> method) {
|
||||||
|
for (int attempts = 0; attempts < maxAttempts; attempts++) {
|
||||||
|
if (method.get()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RbacObjectRepository extends Repository<RbacObjectEntity, UUID> {
|
interface RbacObjectRepository extends Repository<RbacObjectEntity, UUID> {
|
||||||
|
Loading…
Reference in New Issue
Block a user