hierarchical validation for hosting-assets, no concrete rules yet

This commit is contained in:
Michael Hoennig 2024-06-11 09:44:57 +02:00
parent 1d2a65ac22
commit 7b63d867e0
11 changed files with 100 additions and 47 deletions

View File

@ -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);
}
}

View File

@ -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(),

View File

@ -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

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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}",

View File

@ -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

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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