hosting-asset-validation-for-cloud-server-to-webspace (#54)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: #54 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
parent
6c25dddcda
commit
2e9e5d6ef0
@ -1,5 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator;
|
||||||
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;
|
||||||
@ -15,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
@ -60,7 +62,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
|
|
||||||
final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||||
|
|
||||||
final var saved = assetRepo.save(entityToSave);
|
final var saved = assetRepo.save(valid(entityToSave));
|
||||||
|
|
||||||
final var uri =
|
final var uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
@ -120,6 +122,14 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) {
|
||||||
|
final var violations = HsHostingAssetValidator.forType(entityToSave.getType()).validate(entityToSave);
|
||||||
|
if (!violations.isEmpty()) {
|
||||||
|
throw new ValidationException(violations.toString());
|
||||||
|
}
|
||||||
|
return entityToSave;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
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()));
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<List<String>> listAssetTypes() {
|
||||||
|
final var resource = HsHostingAssetValidator.types().stream()
|
||||||
|
.map(Enum::name)
|
||||||
|
.toList();
|
||||||
|
return ResponseEntity.ok(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<List<Object>> listAssetTypeProps(
|
||||||
|
final HsHostingAssetTypeResource assetType) {
|
||||||
|
|
||||||
|
final var propValidators = HsHostingAssetValidator.forType(HsHostingAssetType.of(assetType));
|
||||||
|
final List<Map<String, Object>> resource = propValidators.properties();
|
||||||
|
return ResponseEntity.ok(toListOfObjects(resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Object> toListOfObjects(final List<Map<String, Object>> resource) {
|
||||||
|
// OpenApi ony generates List<Object> not List<Map<String, Object>> for the Java interface.
|
||||||
|
// But Spring properly converts the List of Maps, thus we can simply cast the type:
|
||||||
|
//noinspection rawtypes,unchecked
|
||||||
|
return (List) resource;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.hosting.asset.validator;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||||
|
|
||||||
|
import java.util.AbstractMap.SimpleImmutableEntry;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public abstract class HsHostingAssetPropertyValidator<T> {
|
||||||
|
|
||||||
|
final Class<T> type;
|
||||||
|
final String propertyName;
|
||||||
|
private Boolean required;
|
||||||
|
|
||||||
|
public static <K, V> Map.Entry<K, V> defType(K k, V v) {
|
||||||
|
return new SimpleImmutableEntry<>(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HsHostingAssetPropertyValidator<T> required() {
|
||||||
|
required = Boolean.TRUE;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HsHostingAssetPropertyValidator<T> optional() {
|
||||||
|
required = Boolean.FALSE;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final List<String> validate(final Map<String, Object> props) {
|
||||||
|
final var result = new ArrayList<String>();
|
||||||
|
final var propValue = props.get(propertyName);
|
||||||
|
if (propValue == null) {
|
||||||
|
if (required) {
|
||||||
|
result.add("'" + propertyName + "' is required but missing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (propValue != null){
|
||||||
|
if ( type.isInstance(propValue)) {
|
||||||
|
//noinspection unchecked
|
||||||
|
validate(result, (T) propValue, props);
|
||||||
|
} else {
|
||||||
|
result.add("'" + propertyName + "' is expected to be of type " + type + ", " +
|
||||||
|
"but is of type '" + propValue.getClass().getSimpleName() + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void validate(final ArrayList<String> result, final T propValue, final Map<String, Object> props);
|
||||||
|
|
||||||
|
public void verifyConsistency(final Map.Entry<HsHostingAssetType, HsHostingAssetValidator> typeDef) {
|
||||||
|
if (required == null ) {
|
||||||
|
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> toMap(final ObjectMapper mapper) {
|
||||||
|
final Map<String, Object> map = mapper.convertValue(this, Map.class);
|
||||||
|
map.put("type", simpleTypeName());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract String simpleTypeName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
class IntegerPropertyValidator extends HsHostingAssetPropertyValidator<Integer>{
|
||||||
|
|
||||||
|
private String unit;
|
||||||
|
private Integer min;
|
||||||
|
private Integer max;
|
||||||
|
private Integer step;
|
||||||
|
|
||||||
|
public static IntegerPropertyValidator integerProperty(final String propertyName) {
|
||||||
|
return new IntegerPropertyValidator(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntegerPropertyValidator(final String propertyName) {
|
||||||
|
super(Integer.class, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validate(final ArrayList<String> result, final Integer propValue, final Map<String, Object> props) {
|
||||||
|
if (min != null && propValue < min) {
|
||||||
|
result.add("'" + propertyName + "' is expected to be >= " + min + " but is " + propValue);
|
||||||
|
}
|
||||||
|
if (max != null && propValue > max) {
|
||||||
|
result.add("'" + propertyName + "' is expected to be <= " + max + " but is " + propValue);
|
||||||
|
}
|
||||||
|
if (step != null && propValue % step != 0) {
|
||||||
|
result.add("'" + propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String simpleTypeName() {
|
||||||
|
return "integer";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
class EnumPropertyValidator extends HsHostingAssetPropertyValidator<String> {
|
||||||
|
|
||||||
|
private String[] values;
|
||||||
|
|
||||||
|
private EnumPropertyValidator(final String propertyName) {
|
||||||
|
super(String.class, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EnumPropertyValidator enumerationProperty(final String propertyName) {
|
||||||
|
return new EnumPropertyValidator(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HsHostingAssetPropertyValidator<String> values(final String... values) {
|
||||||
|
this.values = values;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validate(final ArrayList<String> result, final String propValue, final Map<String, Object> props) {
|
||||||
|
if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
|
||||||
|
result.add("'" + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String simpleTypeName() {
|
||||||
|
return "enumeration";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
class BooleanPropertyValidator extends HsHostingAssetPropertyValidator<Boolean> {
|
||||||
|
|
||||||
|
private Map.Entry<String, String> falseIf;
|
||||||
|
|
||||||
|
private BooleanPropertyValidator(final String propertyName) {
|
||||||
|
super(Boolean.class, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BooleanPropertyValidator booleanProperty(final String propertyName) {
|
||||||
|
return new BooleanPropertyValidator(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
HsHostingAssetPropertyValidator<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
|
||||||
|
this.falseIf = new SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
result.add("'" + propertyName + "' is expected to be false because " +
|
||||||
|
falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String simpleTypeName() {
|
||||||
|
return "boolean";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.hosting.asset.validator;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||||
|
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static java.util.Arrays.stream;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.EnumPropertyValidator.enumerationProperty;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetPropertyValidator.defType;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.BooleanPropertyValidator.booleanProperty;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.validator.IntegerPropertyValidator.integerProperty;
|
||||||
|
|
||||||
|
public class HsHostingAssetValidator {
|
||||||
|
|
||||||
|
private static final Map<HsHostingAssetType, HsHostingAssetValidator> validators = Map.ofEntries(
|
||||||
|
defType(HsHostingAssetType.CLOUD_SERVER, new HsHostingAssetValidator(
|
||||||
|
integerProperty("CPUs").min(1).max(32).required(),
|
||||||
|
integerProperty("RAM").unit("GB").min(1).max(128).required(),
|
||||||
|
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-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional())),
|
||||||
|
defType(HsHostingAssetType.MANAGED_SERVER, new HsHostingAssetValidator(
|
||||||
|
integerProperty("CPUs").min(1).max(32).required(),
|
||||||
|
integerProperty("RAM").unit("GB").min(1).max(128).required(),
|
||||||
|
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(),
|
||||||
|
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(),
|
||||||
|
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),
|
||||||
|
booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(),
|
||||||
|
booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(),
|
||||||
|
booleanProperty("SLA-Web").falseIf("SLA-Platform", "BASIC").optional())),
|
||||||
|
defType(HsHostingAssetType.MANAGED_WEBSPACE, new HsHostingAssetValidator(
|
||||||
|
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
|
||||||
|
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
|
||||||
|
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(),
|
||||||
|
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(),
|
||||||
|
integerProperty("Daemons").min(0).max(10).optional(),
|
||||||
|
booleanProperty("Online Office Server").optional())
|
||||||
|
));
|
||||||
|
static {
|
||||||
|
validators.entrySet().forEach(typeDef -> {
|
||||||
|
stream(typeDef.getValue().propertyValidators).forEach( entry -> {
|
||||||
|
entry.verifyConsistency(typeDef);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private final HsHostingAssetPropertyValidator<?>[] propertyValidators;
|
||||||
|
|
||||||
|
public static HsHostingAssetValidator forType(final HsHostingAssetType type) {
|
||||||
|
return validators.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
HsHostingAssetValidator(final HsHostingAssetPropertyValidator<?>... validators) {
|
||||||
|
propertyValidators = validators;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<HsHostingAssetType> types() {
|
||||||
|
return validators.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> validate(final HsHostingAssetEntity assetEntity) {
|
||||||
|
final var result = new ArrayList<String>();
|
||||||
|
assetEntity.getConfig().keySet().forEach( givenPropName -> {
|
||||||
|
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
|
||||||
|
result.add("'" + givenPropName + "' is not expected but is '" +assetEntity.getConfig().get(givenPropName) + "'");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream(propertyValidators).forEach(pv -> {
|
||||||
|
result.addAll(pv.validate(assetEntity.getConfig()));
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> properties() {
|
||||||
|
final var mapper = new ObjectMapper();
|
||||||
|
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||||
|
return Arrays.stream(propertyValidators)
|
||||||
|
.map(propertyValidator -> propertyValidator.toMap(mapper))
|
||||||
|
.map(HsHostingAssetValidator::asKeyValueMap)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
private static Map<String, Object> asKeyValueMap(final Map map) {
|
||||||
|
return (Map<String, Object>) map;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
lombok.addLombokGeneratedAnnotation = true
|
||||||
|
lombok.accessors.chain = true
|
||||||
|
lombok.accessors.fluent = true
|
@ -80,18 +80,85 @@ components:
|
|||||||
# forces generating a java.lang.Object containing a Map, instead of class AssetConfiguration
|
# forces generating a java.lang.Object containing a Map, instead of class AssetConfiguration
|
||||||
anyOf:
|
anyOf:
|
||||||
- type: object
|
- type: object
|
||||||
properties:
|
# single source of supported properties just via /api/hs/hosting/asset-types/{assetType}
|
||||||
CPU:
|
# TODO.impl: later, we could generate the config types and their properties from the validation config
|
||||||
type: integer
|
|
||||||
minimum: 1
|
|
||||||
maximum: 16
|
|
||||||
SSD:
|
|
||||||
type: integer
|
|
||||||
minimum: 16
|
|
||||||
maximum: 4096
|
|
||||||
HDD:
|
|
||||||
type: integer
|
|
||||||
minimum: 16
|
|
||||||
maximum: 4096
|
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
|
|
||||||
|
PropertyDescriptor:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
"type":
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- integer
|
||||||
|
- boolean
|
||||||
|
- enumeration
|
||||||
|
"propertyName":
|
||||||
|
type: string
|
||||||
|
pattern: "^[ a-zA-Z0-9_-]$"
|
||||||
|
"required":
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
- propertyName
|
||||||
|
- required
|
||||||
|
|
||||||
|
IntegerPropertyDescriptor:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/PropertyDescriptor'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
"type":
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- integer
|
||||||
|
"unit":
|
||||||
|
type: string
|
||||||
|
"min":
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
"max":
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
"step":
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
required:
|
||||||
|
- "type"
|
||||||
|
- "propertyName"
|
||||||
|
- "required"
|
||||||
|
|
||||||
|
BooleanPropertyDescriptor:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/PropertyDescriptor'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
"type":
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- boolean
|
||||||
|
"falseIf":
|
||||||
|
type: object
|
||||||
|
anyOf:
|
||||||
|
- type: object
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
EnumerationPropertyDescriptor:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/PropertyDescriptor'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
"type":
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- enumeration
|
||||||
|
"values":
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
HsHostingAssetProps:
|
||||||
|
anyOf:
|
||||||
|
- $ref: '#/components/schemas/IntegerPropertyDescriptor'
|
||||||
|
- $ref: '#/components/schemas/BooleanPropertyDescriptor'
|
||||||
|
- $ref: '#/components/schemas/EnumerationPropertyDescriptor'
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
get:
|
||||||
|
summary: Returns a list of available asset properties for the given type.
|
||||||
|
description: Returns the list of available properties and their validations for a given asset type.
|
||||||
|
tags:
|
||||||
|
- hs-hosting-asset-props
|
||||||
|
operationId: listAssetTypeProps
|
||||||
|
parameters:
|
||||||
|
- name: assetType
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetType'
|
||||||
|
description: The asset type whose properties are to be returned.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetProps'
|
||||||
|
"401":
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
|
"403":
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Forbidden'
|
@ -0,0 +1,19 @@
|
|||||||
|
get:
|
||||||
|
summary: Returns a list of available asset types.
|
||||||
|
description: Returns the list of asset types to enable an adaptive UI.
|
||||||
|
tags:
|
||||||
|
- hs-hosting-asset-props
|
||||||
|
operationId: listAssetTypes
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
"401":
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
|
"403":
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Forbidden'
|
@ -13,18 +13,20 @@ get:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
description: The UUID of the debitor, whose hosting assets are to be listed.
|
||||||
- name: parentAssetUuid
|
- name: parentAssetUuid
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
description: The UUID of the parentAsset, whose hosting assets are to be listed.
|
||||||
- name: type
|
- name: type
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
$ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetType'
|
$ref: 'hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetType'
|
||||||
description: The UUID of the debitor, whose hosting assets are to be listed.
|
description: The type of hosting assets to be listed.
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
|
@ -8,10 +8,18 @@ servers:
|
|||||||
|
|
||||||
paths:
|
paths:
|
||||||
|
|
||||||
# Items
|
# Assets
|
||||||
|
|
||||||
/api/hs/hosting/assets:
|
/api/hs/hosting/assets:
|
||||||
$ref: "hs-hosting-assets.yaml"
|
$ref: "hs-hosting-assets.yaml"
|
||||||
|
|
||||||
/api/hs/hosting/assets/{assetUuid}:
|
/api/hs/hosting/assets/{assetUuid}:
|
||||||
$ref: "hs-hosting-assets-with-uuid.yaml"
|
$ref: "hs-hosting-assets-with-uuid.yaml"
|
||||||
|
|
||||||
|
# Asset-Types
|
||||||
|
|
||||||
|
/api/hs/hosting/asset-types:
|
||||||
|
$ref: "hs-hosting-asset-types.yaml"
|
||||||
|
|
||||||
|
/api/hs/hosting/asset-types/{assetType}:
|
||||||
|
$ref: "hs-hosting-asset-types-props.yaml"
|
||||||
|
@ -52,6 +52,7 @@ public class ArchitectureTest {
|
|||||||
"..hs.office.sepamandate",
|
"..hs.office.sepamandate",
|
||||||
"..hs.booking.item",
|
"..hs.booking.item",
|
||||||
"..hs.hosting.asset",
|
"..hs.hosting.asset",
|
||||||
|
"..hs.hosting.asset.validator",
|
||||||
"..errors",
|
"..errors",
|
||||||
"..mapper",
|
"..mapper",
|
||||||
"..ping",
|
"..ping",
|
||||||
|
@ -174,7 +174,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"type": "MANAGED_SERVER",
|
"type": "MANAGED_SERVER",
|
||||||
"identifier": "vm1400",
|
"identifier": "vm1400",
|
||||||
"caption": "some new CloudServer",
|
"caption": "some new CloudServer",
|
||||||
"config": { "CPU": 3, "extra": 42 }
|
"config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 }
|
||||||
}
|
}
|
||||||
""".formatted(givenBookingItem.getUuid()))
|
""".formatted(givenBookingItem.getUuid()))
|
||||||
.port(port)
|
.port(port)
|
||||||
@ -188,7 +188,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"type": "MANAGED_SERVER",
|
"type": "MANAGED_SERVER",
|
||||||
"identifier": "vm1400",
|
"identifier": "vm1400",
|
||||||
"caption": "some new CloudServer",
|
"caption": "some new CloudServer",
|
||||||
"config": { "CPU": 3, "extra": 42 }
|
"config": { "CPUs": 2, "RAM": 100, "SSD": 300, "Traffic": 250 }
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
|
.header("Location", matchesRegex("http://localhost:[1-9][0-9]*/api/hs/hosting/assets/[^/]*"))
|
||||||
@ -199,6 +199,39 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
location.substring(location.lastIndexOf('/') + 1));
|
location.substring(location.lastIndexOf('/') + 1));
|
||||||
assertThat(newUserUuid).isNotNull();
|
assertThat(newUserUuid).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void additionalValidationsArePerformend_whenAddingAsset() {
|
||||||
|
|
||||||
|
context.define("superuser-alex@hostsharing.net");
|
||||||
|
final var givenBookingItem = givenBookingItem("First", "some PrivateCloud");
|
||||||
|
|
||||||
|
final var location = RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("current-user", "superuser-alex@hostsharing.net")
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"bookingItemUuid": "%s",
|
||||||
|
"type": "MANAGED_SERVER",
|
||||||
|
"identifier": "vm1400",
|
||||||
|
"caption": "some new CloudServer",
|
||||||
|
"config": { "CPUs": 0, "extra": 42 }
|
||||||
|
}
|
||||||
|
""".formatted(givenBookingItem.getUuid()))
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.post("http://localhost/api/hs/hosting/assets")
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(400)
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("", lenientlyEquals("""
|
||||||
|
{
|
||||||
|
"statusPhrase": "Bad Request",
|
||||||
|
"message": "['extra' is not expected but is '42', 'CPUs' is expected to be >= 1 but is 0, 'RAM' is required but missing, 'SSD' is required but missing, 'Traffic' is required but missing]"
|
||||||
|
}
|
||||||
|
""")); // @formatter:on
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
@ -0,0 +1,156 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||||
|
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||||
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
|
||||||
|
|
||||||
|
@SpringBootTest(
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||||
|
classes = { HsadminNgApplication.class, JpaAttempt.class }
|
||||||
|
)
|
||||||
|
class HsHostingAssetPropsControllerAcceptanceTest {
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private Integer port;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anyone_canListAvailableAssetTypes() {
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.get("http://localhost/api/hs/hosting/asset-types")
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("", lenientlyEquals("""
|
||||||
|
[
|
||||||
|
"MANAGED_SERVER",
|
||||||
|
"MANAGED_WEBSPACE",
|
||||||
|
"CLOUD_SERVER"
|
||||||
|
]
|
||||||
|
"""));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void globalAdmin_canListPropertiesOfGivenAssetType() {
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.get("http://localhost/api/hs/hosting/asset-types/" + HsHostingAssetType.MANAGED_SERVER)
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("", lenientlyEquals("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"propertyName": "CPUs",
|
||||||
|
"required": true,
|
||||||
|
"unit": null,
|
||||||
|
"min": 1,
|
||||||
|
"max": 32,
|
||||||
|
"step": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"propertyName": "RAM",
|
||||||
|
"required": true,
|
||||||
|
"unit": "GB",
|
||||||
|
"min": 1,
|
||||||
|
"max": 128,
|
||||||
|
"step": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"propertyName": "SSD",
|
||||||
|
"required": true,
|
||||||
|
"unit": "GB",
|
||||||
|
"min": 25,
|
||||||
|
"max": 1000,
|
||||||
|
"step": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"propertyName": "HDD",
|
||||||
|
"required": false,
|
||||||
|
"unit": "GB",
|
||||||
|
"min": 0,
|
||||||
|
"max": 4000,
|
||||||
|
"step": 250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"propertyName": "Traffic",
|
||||||
|
"required": true,
|
||||||
|
"unit": "GB",
|
||||||
|
"min": 250,
|
||||||
|
"max": 10000,
|
||||||
|
"step": 250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "enumeration",
|
||||||
|
"propertyName": "SLA-Platform",
|
||||||
|
"required": false,
|
||||||
|
"values": [
|
||||||
|
"BASIC",
|
||||||
|
"EXT8H",
|
||||||
|
"EXT4H",
|
||||||
|
"EXT2H"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "SLA-EMail",
|
||||||
|
"required": false,
|
||||||
|
"falseIf": {
|
||||||
|
"SLA-Platform": "BASIC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "SLA-Maria",
|
||||||
|
"required": false,
|
||||||
|
"falseIf": {
|
||||||
|
"SLA-Platform": "BASIC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "SLA-PgSQL",
|
||||||
|
"required": false,
|
||||||
|
"falseIf": {
|
||||||
|
"SLA-Platform": "BASIC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "SLA-Office",
|
||||||
|
"required": false,
|
||||||
|
"falseIf": {
|
||||||
|
"SLA-Platform": "BASIC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"propertyName": "SLA-Web",
|
||||||
|
"required": false,
|
||||||
|
"falseIf": {
|
||||||
|
"SLA-Platform": "BASIC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.validator.HsHostingAssetValidator;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyMap;
|
||||||
|
import static java.util.Map.entry;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class HsHostingAssetValidatorUnitTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatesMissingProperties() {
|
||||||
|
// given
|
||||||
|
final var validator = HsHostingAssetValidator.forType(MANAGED_WEBSPACE);
|
||||||
|
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
|
||||||
|
.type(MANAGED_WEBSPACE)
|
||||||
|
.config(emptyMap())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactlyInAnyOrder(
|
||||||
|
"'SSD' is required but missing",
|
||||||
|
"'Traffic' is required but missing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatesUnknownProperties() {
|
||||||
|
// given
|
||||||
|
final var validator = HsHostingAssetValidator.forType(MANAGED_WEBSPACE);
|
||||||
|
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
|
||||||
|
.type(MANAGED_WEBSPACE)
|
||||||
|
.config(Map.ofEntries(
|
||||||
|
entry("HDD", 0),
|
||||||
|
entry("SSD", 1),
|
||||||
|
entry("Traffic", 10),
|
||||||
|
entry("unknown", "some value")
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("'unknown' is not expected but is 'some value'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatesDependentProperties() {
|
||||||
|
// given
|
||||||
|
final var validator = HsHostingAssetValidator.forType(MANAGED_SERVER);
|
||||||
|
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
|
||||||
|
.type(MANAGED_SERVER)
|
||||||
|
.config(Map.ofEntries(
|
||||||
|
entry("CPUs", 2),
|
||||||
|
entry("RAM", 25),
|
||||||
|
entry("SSD", 25),
|
||||||
|
entry("Traffic", 250),
|
||||||
|
entry("SLA-EMail", true)
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).containsExactly("'SLA-EMail' is expected to be false because SLA-Platform=BASIC but is true");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatesValidProperties() {
|
||||||
|
// given
|
||||||
|
final var validator = HsHostingAssetValidator.forType(MANAGED_WEBSPACE);
|
||||||
|
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
|
||||||
|
.type(MANAGED_WEBSPACE)
|
||||||
|
.config(Map.ofEntries(
|
||||||
|
entry("HDD", 200),
|
||||||
|
entry("SSD", 25),
|
||||||
|
entry("Traffic", 250)
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = validator.validate(mangedWebspaceHostingAssetEntity);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(result).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user