hierarchical validation for hosting-assets, no concrete rules yet
This commit is contained in:
parent
1d2a65ac22
commit
7b63d867e0
@ -11,7 +11,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.Collections.emptyList;
|
||||
@ -49,6 +48,10 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
|
||||
return validators.keySet();
|
||||
}
|
||||
|
||||
public static List<String> doValidate(final HsBookingItemEntity bookingItem) {
|
||||
return HsBookingItemEntityValidator.forType(bookingItem.getType()).validate(bookingItem);
|
||||
}
|
||||
|
||||
public static HsBookingItemEntity valid(final HsBookingItemEntity entityToSave) {
|
||||
final var violations = doValidate(entityToSave);
|
||||
if (!violations.isEmpty()) {
|
||||
@ -57,10 +60,6 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
|
||||
return entityToSave;
|
||||
}
|
||||
|
||||
public static List<String> doValidate(final HsBookingItemEntity bookingItem) {
|
||||
return HsBookingItemEntityValidator.forType(bookingItem.getType()).validate(bookingItem);
|
||||
}
|
||||
|
||||
public HsBookingItemEntityValidator(final ValidatableProperty<?>... properties) {
|
||||
super(properties);
|
||||
}
|
||||
@ -79,21 +78,12 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
|
||||
|
||||
protected List<String> validateSubEntities(final HsBookingItemEntity bookingItem) {
|
||||
return stream(propertyValidators)
|
||||
.filter(propDef -> propDef.isTotalsValidator())
|
||||
.filter(ValidatableProperty::isTotalsValidator)
|
||||
.map(prop -> validateMaxTotalValue(bookingItem, prop))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private List<String> sequentiallyValidate(final Supplier<List<String>>... validators) {
|
||||
return stream(validators)
|
||||
.map(Supplier::get)
|
||||
.filter(violations -> !violations.isEmpty())
|
||||
.findFirst()
|
||||
.orElse(emptyList());
|
||||
}
|
||||
|
||||
private String validateMaxTotalValue(
|
||||
final HsBookingItemEntity bookingItem,
|
||||
final ValidatableProperty<?> propDef) {
|
||||
@ -110,11 +100,4 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
|
||||
propName, totalValue, propUnit, propName, maxValue, propUnit)
|
||||
: null;
|
||||
}
|
||||
|
||||
private static Integer toInteger(final Object value) {
|
||||
if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
}
|
||||
throw new IllegalArgumentException("Integer value expected, but got " + value);
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class HsManagedServerBookingItemValidator extends HsBookingItemEntityValidator {
|
||||
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
|
||||
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
|
||||
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
|
||||
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(),
|
||||
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC"),
|
||||
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(),
|
||||
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),
|
||||
booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(),
|
||||
|
@ -16,6 +16,7 @@ import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
@ -24,11 +25,13 @@ import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Transient;
|
||||
import jakarta.persistence.Version;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -90,6 +93,10 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject {
|
||||
@Enumerated(EnumType.STRING)
|
||||
private HsHostingAssetType type;
|
||||
|
||||
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true)
|
||||
@JoinColumn(name="parentassetuuid", referencedColumnName="uuid")
|
||||
private List<HsHostingAssetEntity> subHostingAssets;
|
||||
|
||||
@Column(name = "identifier")
|
||||
private String identifier; // vm1234, xyz00, example.org, xyz00_abc
|
||||
|
||||
|
@ -6,16 +6,17 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
|
||||
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
|
||||
import org.apache.commons.collections4.ListUtils;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
|
||||
@ -37,22 +38,28 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAs
|
||||
}
|
||||
|
||||
public static HsEntityValidator<HsHostingAssetEntity> forType(final Enum<HsHostingAssetType> type) {
|
||||
return validators.get(type);
|
||||
if ( validators.containsKey(type)) {
|
||||
return validators.get(type);
|
||||
}
|
||||
throw new IllegalArgumentException("no validator found for type " + type);
|
||||
}
|
||||
|
||||
public static Set<Enum<HsHostingAssetType>> types() {
|
||||
return validators.keySet();
|
||||
}
|
||||
|
||||
public static List<String> doValidate(final HsHostingAssetEntity hostingAsset) {
|
||||
return HsHostingAssetEntityValidator.forType(hostingAsset.getType()).validate(hostingAsset);
|
||||
}
|
||||
|
||||
public static HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) {
|
||||
final var violations = HsHostingAssetEntityValidator.forType(entityToSave.getType()).validate(entityToSave);
|
||||
final var violations = doValidate(entityToSave);
|
||||
if (!violations.isEmpty()) {
|
||||
throw new ValidationException(violations.toString());
|
||||
}
|
||||
return entityToSave;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public HsHostingAssetEntityValidator(final ValidatableProperty<?>... properties) {
|
||||
super(properties);
|
||||
}
|
||||
@ -60,13 +67,12 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAs
|
||||
|
||||
@Override
|
||||
public List<String> validate(final HsHostingAssetEntity assetEntity) {
|
||||
final var selfValidation = enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig()));
|
||||
return selfValidation.isEmpty()
|
||||
// higher levels are only validated with valid sub-entity
|
||||
? ListUtils.union(
|
||||
optionallyValidate(assetEntity.getParentAsset()),
|
||||
optionallyValidate(assetEntity.getBookingItem()))
|
||||
: selfValidation;
|
||||
return sequentiallyValidate(
|
||||
() -> enrich(prefix(assetEntity.toShortString(), "config"), validateProperties(assetEntity.getConfig())),
|
||||
() -> enrich(prefix(assetEntity.toShortString(), "bookingItem"), optionallyValidate(assetEntity.getBookingItem())),
|
||||
() -> enrich(prefix(assetEntity.toShortString(), "parentAsset"), optionallyValidate(assetEntity.getParentAsset())),
|
||||
() -> validateSubEntities(assetEntity)
|
||||
);
|
||||
}
|
||||
|
||||
private static List<String> optionallyValidate(final HsHostingAssetEntity assetEntity) {
|
||||
@ -76,4 +82,29 @@ public class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAs
|
||||
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
|
||||
return bookingItem != null ? HsBookingItemEntityValidator.doValidate(bookingItem) : emptyList();
|
||||
}
|
||||
|
||||
protected List<String> validateSubEntities(final HsHostingAssetEntity assetEntity) {
|
||||
return stream(propertyValidators)
|
||||
.filter(ValidatableProperty::isTotalsValidator)
|
||||
.map(prop -> validateMaxTotalValue(assetEntity, prop))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String validateMaxTotalValue(
|
||||
final HsHostingAssetEntity hostingAsset,
|
||||
final ValidatableProperty<?> propDef) {
|
||||
final var propName = propDef.propertyName();
|
||||
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
|
||||
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
|
||||
.stream()
|
||||
.map(subItem -> propDef.getValue(subItem.getConfig()))
|
||||
.map(HsEntityValidator::toInteger)
|
||||
.reduce(0, Integer::sum);
|
||||
final var maxValue = toInteger(propDef.getValue(hostingAsset.getConfig()));
|
||||
return totalValue > maxValue
|
||||
? "total %s is %d%s exceeds max total %s %d%s".formatted(
|
||||
propName, totalValue, propUnit, propName, maxValue, propUnit)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
@ -27,10 +27,11 @@ public class BooleanProperty extends ValidatableProperty<Boolean> {
|
||||
|
||||
@Override
|
||||
protected void validate(final ArrayList<String> result, final Boolean propValue, final Map<String, Object> props) {
|
||||
if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) {
|
||||
if (propValue) {
|
||||
if (falseIf != null && propValue) {
|
||||
final Object referencedValue = props.get(falseIf.getKey());
|
||||
if (Objects.equals(referencedValue, falseIf.getValue())) {
|
||||
result.add(propertyName + " is expected to be false because " +
|
||||
falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue);
|
||||
falseIf.getKey() + "=" + referencedValue + " but is " + propValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.Collections.emptyList;
|
||||
@ -66,4 +67,20 @@ public abstract class HsEntityValidator<E> {
|
||||
? HsBookingItemEntityValidator.doValidate(bookingItem.getParentItem())
|
||||
: emptyList();
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
protected static List<String> sequentiallyValidate(final Supplier<List<String>>... validators) {
|
||||
return new ArrayList<>(stream(validators)
|
||||
.map(Supplier::get)
|
||||
.filter(violations -> !violations.isEmpty())
|
||||
.findFirst()
|
||||
.orElse(emptyList()));
|
||||
}
|
||||
|
||||
protected static Integer toInteger(final Object value) {
|
||||
if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
}
|
||||
throw new IllegalArgumentException("Integer value expected, but got " + value);
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ class HsManagedServerBookingItemValidatorUnitTest {
|
||||
entry("RAM", 25),
|
||||
entry("SSD", 25),
|
||||
entry("Traffic", 250),
|
||||
entry("SLA-Platform", "BASIC"),
|
||||
entry("SLA-EMail", true)
|
||||
))
|
||||
.build();
|
||||
@ -60,7 +61,7 @@ class HsManagedServerBookingItemValidatorUnitTest {
|
||||
"{type=integer, propertyName=SSD, required=true, defaultValue=null, asTotalLimitValidator=null, unit=GB, min=25, max=1000, step=25, totalsValidator=false}",
|
||||
"{type=integer, propertyName=HDD, required=false, defaultValue=null, asTotalLimitValidator=null, unit=GB, min=0, max=4000, step=250, totalsValidator=false}",
|
||||
"{type=integer, propertyName=Traffic, required=true, defaultValue=null, asTotalLimitValidator=null, unit=GB, min=250, max=10000, step=250, totalsValidator=false}",
|
||||
"{type=enumeration, propertyName=SLA-Platform, required=false, defaultValue=null, asTotalLimitValidator=null, values=[BASIC, EXT8H, EXT4H, EXT2H], totalsValidator=false}",
|
||||
"{type=enumeration, propertyName=SLA-Platform, required=false, defaultValue=BASIC, asTotalLimitValidator=null, values=[BASIC, EXT8H, EXT4H, EXT2H], totalsValidator=false}",
|
||||
"{type=boolean, propertyName=SLA-EMail, required=false, defaultValue=null, asTotalLimitValidator=null, falseIf={SLA-Platform=BASIC}, totalsValidator=false}",
|
||||
"{type=boolean, propertyName=SLA-Maria, required=false, defaultValue=null, asTotalLimitValidator=null, falseIf={SLA-Platform=BASIC}, totalsValidator=false}",
|
||||
"{type=boolean, propertyName=SLA-PgSQL, required=false, defaultValue=null, asTotalLimitValidator=null, falseIf={SLA-Platform=BASIC}, totalsValidator=false}",
|
||||
|
@ -17,6 +17,7 @@ class HsCloudServerHostingAssetValidatorUnitTest {
|
||||
// given
|
||||
final var cloudServerHostingAssetEntity = HsHostingAssetEntity.builder()
|
||||
.type(CLOUD_SERVER)
|
||||
.identifier("vm1234")
|
||||
.config(Map.ofEntries(
|
||||
entry("RAM", 2000)
|
||||
))
|
||||
@ -28,7 +29,7 @@ class HsCloudServerHostingAssetValidatorUnitTest {
|
||||
final var result = validator.validate(cloudServerHostingAssetEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).containsExactly("'config.RAM' is not expected but is set to '2000'");
|
||||
assertThat(result).containsExactly("CLOUD_SERVER:vm1234.config.RAM is not expected but is set to '2000'");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -17,6 +17,7 @@ class HsHostingAssetEntityValidatorUnitTest {
|
||||
// given
|
||||
final var managedServerHostingAssetEntity = HsHostingAssetEntity.builder()
|
||||
.type(MANAGED_SERVER)
|
||||
.identifier("vm1234")
|
||||
.build();
|
||||
|
||||
// when
|
||||
@ -25,8 +26,8 @@ class HsHostingAssetEntityValidatorUnitTest {
|
||||
// then
|
||||
assertThat(result).isInstanceOf(ValidationException.class)
|
||||
.hasMessageContaining(
|
||||
"'config.monit_max_ssd_usage' is required but missing",
|
||||
"'config.monit_max_cpu_usage' is required but missing",
|
||||
"'config.monit_max_ram_usage' is required but missing");
|
||||
"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");
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ class HsManagedServerHostingAssetValidatorUnitTest {
|
||||
// given
|
||||
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
|
||||
.type(MANAGED_SERVER)
|
||||
.identifier("vm1234")
|
||||
.config(Map.ofEntries(
|
||||
entry("monit_max_hdd_usage", "90"),
|
||||
entry("monit_max_cpu_usage", 2),
|
||||
@ -30,9 +31,9 @@ class HsManagedServerHostingAssetValidatorUnitTest {
|
||||
|
||||
// then
|
||||
assertThat(result).containsExactlyInAnyOrder(
|
||||
"'config.monit_max_ssd_usage' is required but missing",
|
||||
"'config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'",
|
||||
"'config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
|
||||
"'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_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");
|
||||
}
|
||||
}
|
||||
|
@ -18,10 +18,20 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
|
||||
final HsBookingItemEntity managedServerBookingItem = HsBookingItemEntity.builder()
|
||||
.project(TEST_PROJECT)
|
||||
.type(HsBookingItemType.MANAGED_SERVER)
|
||||
.caption("Test Managed-Server")
|
||||
.resources(Map.ofEntries(
|
||||
entry("CPUs", 2),
|
||||
entry("RAM", 25),
|
||||
entry("SSD", 25),
|
||||
entry("Traffic", 250),
|
||||
entry("SLA-Platform", "EXT4H"),
|
||||
entry("SLA-EMail", true)
|
||||
))
|
||||
.build();
|
||||
final HsHostingAssetEntity mangedServerAssetEntity = HsHostingAssetEntity.builder()
|
||||
.type(HsHostingAssetType.MANAGED_SERVER)
|
||||
.bookingItem(managedServerBookingItem)
|
||||
.identifier("vm1234")
|
||||
.config(Map.ofEntries(
|
||||
entry("monit_max_ssd_usage", 70),
|
||||
entry("monit_max_cpu_usage", 80),
|
||||
@ -63,7 +73,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
|
||||
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
|
||||
|
||||
// then
|
||||
assertThat(result).containsExactly("'config.unknown' is not expected but is set to 'some value'");
|
||||
assertThat(result).containsExactly("MANAGED_WEBSPACE:abc00.config.unknown is not expected but is set to 'some value'");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Loading…
Reference in New Issue
Block a user