package io.cattle.platform.servicediscovery.api.filter; import static io.cattle.platform.core.model.tables.ServiceTable.*; import io.cattle.platform.core.addon.PortRule; import io.cattle.platform.core.addon.ScalePolicy; import io.cattle.platform.core.constants.CommonStatesConstants; import io.cattle.platform.core.constants.InstanceConstants; import io.cattle.platform.core.constants.ServiceConstants; import io.cattle.platform.core.model.Service; import io.cattle.platform.core.model.Stack; import io.cattle.platform.core.util.PortSpec; import io.cattle.platform.iaas.api.filter.common.AbstractDefaultResourceManagerFilter; import io.cattle.platform.json.JsonMapper; import io.cattle.platform.object.ObjectManager; import io.cattle.platform.object.util.DataAccessor; import io.cattle.platform.object.util.DataUtils; import io.cattle.platform.servicediscovery.api.util.ServiceDiscoveryUtil; import io.cattle.platform.servicediscovery.api.util.selector.SelectorUtils; import io.cattle.platform.storage.api.filter.ExternalTemplateInstanceFilter; import io.cattle.platform.storage.service.StorageService; import io.cattle.platform.util.type.CollectionUtils; import io.github.ibuildthecloud.gdapi.exception.ValidationErrorException; import io.github.ibuildthecloud.gdapi.request.ApiRequest; import io.github.ibuildthecloud.gdapi.request.resource.ResourceManager; import io.github.ibuildthecloud.gdapi.validation.ValidationErrorCodes; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import javax.inject.Named; import org.apache.commons.lang3.StringUtils; @Named public class ServiceCreateValidationFilter extends AbstractDefaultResourceManagerFilter { @Inject ObjectManager objectManager; @Inject StorageService storageService; @Inject JsonMapper jsonMapper; private static final int LB_HEALTH_CHECK_PORT = 42; @Override public Class<?>[] getTypeClasses() { return new Class<?>[] { Service.class }; } @Override public String[] getTypes() { return new String[] { ServiceConstants.KIND_SERVICE, ServiceConstants.KIND_LOAD_BALANCER_SERVICE, ServiceConstants.KIND_EXTERNAL_SERVICE, ServiceConstants.KIND_DNS_SERVICE, ServiceConstants.KIND_NETWORK_DRIVER_SERVICE, ServiceConstants.KIND_STORAGE_DRIVER_SERVICE}; } @Override public Object create(String type, ApiRequest request, ResourceManager next) { Service service = request.proxyRequestObject(Service.class); validateStack(service); validateSelector(request); validateMetadata(request); validateLaunchConfigs(service, request); validateIpsHostName(request); request = validateAndSetImage(request, service, type); validatePorts(service, type, request); validateScalePolicy(service, request, false); request = setServiceIndexStrategy(type, request); request = setLBServiceEnvVarsAndHealthcheck(type, service, request); validateLbConfig(request, type); return super.create(type, request, next); } @SuppressWarnings("unchecked") public void validateLbConfig(ApiRequest request, String type) { // add lb information to the metadata if (!type.equalsIgnoreCase(ServiceConstants.KIND_LOAD_BALANCER_SERVICE)) { return; } Map<String, Object> lbConfig = DataUtils.getFieldFromRequest(request, ServiceConstants.FIELD_LB_CONFIG, Map.class); if (lbConfig != null && lbConfig.containsKey(ServiceConstants.FIELD_PORT_RULES)) { List<PortRule> portRules = jsonMapper.convertCollectionValue( lbConfig.get(ServiceConstants.FIELD_PORT_RULES), List.class, PortRule.class); for (PortRule rule : portRules) { // either serviceId or selector are required boolean emptySelector = StringUtils.isEmpty(rule.getSelector()); boolean emptyService = StringUtils.isEmpty(rule.getServiceId()); if (emptySelector && emptyService) { throw new ValidationErrorException(ValidationErrorCodes.MISSING_REQUIRED, "serviceId"); } if (!emptySelector && !emptyService) { throw new ValidationErrorException(ValidationErrorCodes.INVALID_OPTION, "Can't specify both selector and serviceId"); } if (!emptyService && rule.getTargetPort() == null) { throw new ValidationErrorException(ValidationErrorCodes.MISSING_REQUIRED, "targetPort"); } } } } public ApiRequest setServiceIndexStrategy(String type, ApiRequest request) { if (!type.equalsIgnoreCase(ServiceConstants.KIND_SERVICE)) { return request; } Map<String, Object> data = CollectionUtils.toMap(request.getRequestObject()); data.put(ServiceConstants.FIELD_SERVICE_INDEX_STRATEGY, ServiceConstants.SERVICE_INDEX_DU_STRATEGY); request.setRequestObject(data); return request; } @SuppressWarnings("unchecked") public ApiRequest setLBServiceEnvVarsAndHealthcheck(String type, Service lbService, ApiRequest request) { if (!ServiceConstants.KIND_LOAD_BALANCER_SERVICE.equalsIgnoreCase(type)) { return request; } Map<String, Object> data = CollectionUtils.toMap(request.getRequestObject()); if (data.get(ServiceConstants.FIELD_LAUNCH_CONFIG) == null) { return request; } Map<Object, Object> launchConfig = (Map<Object, Object>) data.get(ServiceConstants.FIELD_LAUNCH_CONFIG); ServiceDiscoveryUtil.injectBalancerLabelsAndHealthcheck(launchConfig); data.put(ServiceConstants.FIELD_LAUNCH_CONFIG, launchConfig); request.setRequestObject(data); return request; } public void validatePorts(Service service, String type, ApiRequest request) { validateLBPortRules(type, request); validatePorts(request); } @SuppressWarnings("unchecked") private void validatePorts(ApiRequest request) { Map<String, Object> data = CollectionUtils.toMap(request.getRequestObject()); if (data.get(ServiceConstants.FIELD_LAUNCH_CONFIG) == null) { return; } Map<String, Object> lc = (Map<String, Object>) data.get(ServiceConstants.FIELD_LAUNCH_CONFIG); List<String> ports = jsonMapper.convertCollectionValue( lc.get(InstanceConstants.FIELD_PORTS), List.class, String.class); if (ports != null) { for (Object port : ports) { if (port == null) { throw new ValidationErrorException(ValidationErrorCodes.MISSING_REQUIRED, InstanceConstants.FIELD_PORTS); } /* This will parse the PortSpec and throw an error */ new PortSpec(port.toString()); } } } @SuppressWarnings("unchecked") private void validateLBPortRules(String type, ApiRequest request) { if (!type.equalsIgnoreCase(ServiceConstants.KIND_LOAD_BALANCER_SERVICE)) { return; } Map<String, Object> lbConfig = DataUtils.getFieldFromRequest(request, ServiceConstants.FIELD_LB_CONFIG, Map.class); if (lbConfig == null) { return; } if (lbConfig != null && lbConfig.containsKey(ServiceConstants.FIELD_PORT_RULES)) { List<PortRule> portRules = jsonMapper.convertCollectionValue( lbConfig.get(ServiceConstants.FIELD_PORT_RULES), List.class, PortRule.class); for (PortRule portRule : portRules) { if (portRule.getSourcePort() != null && portRule.getSourcePort().equals(LB_HEALTH_CHECK_PORT)) { throw new ValidationErrorException(ValidationErrorCodes.INVALID_OPTION, "Port " + LB_HEALTH_CHECK_PORT + " is reserved for service health check"); } } } } protected void validateScalePolicy(Service service, ApiRequest request, boolean forUpdate) { Integer scale = DataUtils.getFieldFromRequest(request, ServiceConstants.FIELD_SCALE, Integer.class); if (scale == null && forUpdate) { scale = DataAccessor.fieldInteger(service, ServiceConstants.FIELD_SCALE); } if (scale == null) { return; } Object policyObj = DataUtils.getFieldFromRequest(request, ServiceConstants.FIELD_SCALE_POLICY, Object.class); ScalePolicy policy = null; if (policyObj != null) { policy = jsonMapper.convertValue(policyObj, ScalePolicy.class); } else if (forUpdate) { policy = DataAccessor.field(service, ServiceConstants.FIELD_SCALE_POLICY, jsonMapper, ScalePolicy.class); } if (policy == null) { return; } if (policy.getMin().intValue() > policy.getMax().intValue()) { throw new ValidationErrorException(ValidationErrorCodes.MAX_LIMIT_EXCEEDED, "Min scale can't exceed scale"); } } protected void validateSelector(ApiRequest request) { String selectorContainer = DataUtils.getFieldFromRequest(request, ServiceConstants.FIELD_SELECTOR_CONTAINER, String.class); if (selectorContainer != null) { SelectorUtils.getSelectorConstraints(selectorContainer); } String selectorLink = DataUtils.getFieldFromRequest(request, ServiceConstants.FIELD_SELECTOR_LINK, String.class); if (selectorLink != null) { SelectorUtils.getSelectorConstraints(selectorLink); } } protected void validateMetadata(ApiRequest request) { Object metadata = DataUtils.getFieldFromRequest(request, ServiceConstants.FIELD_METADATA, Object.class); if (metadata != null) { try { String value = jsonMapper.writeValueAsString(metadata); if (value.length() > 1048576) { throw new ValidationErrorException(ValidationErrorCodes.MAX_LIMIT_EXCEEDED, ServiceConstants.FIELD_METADATA + " limit is 1MB"); } } catch (IOException e) { throw new ValidationErrorException(ValidationErrorCodes.INVALID_OPTION, "Failed to serialize field " + ServiceConstants.FIELD_METADATA); } } } protected void validateStack(Service service) { Stack env = objectManager.loadResource(Stack.class, service.getStackId()); List<String> invalidStates = Arrays.asList(InstanceConstants.STATE_ERROR, CommonStatesConstants.REMOVED, CommonStatesConstants.REMOVING); if (env == null || invalidStates.contains(env.getState())) { throw new ValidationErrorException(ValidationErrorCodes.INVALID_STATE, ServiceConstants.FIELD_STACK_ID); } } @SuppressWarnings("unchecked") protected ApiRequest validateAndSetImage(ApiRequest request, Service service, String type) { Map<String, Object> data = CollectionUtils.toMap(request.getRequestObject()); if (data.get(ServiceConstants.FIELD_LAUNCH_CONFIG) != null) { Map<String, Object> launchConfig = (Map<String, Object>)data.get(ServiceConstants.FIELD_LAUNCH_CONFIG); if (launchConfig.get(InstanceConstants.FIELD_IMAGE_UUID) != null) { Object imageUuid = launchConfig.get(InstanceConstants.FIELD_IMAGE_UUID); if (imageUuid != null && !imageUuid.toString().equalsIgnoreCase(ServiceConstants.IMAGE_NONE)) { String fullImageName = ExternalTemplateInstanceFilter.getImageUuid(imageUuid.toString(), storageService); launchConfig.put(InstanceConstants.FIELD_IMAGE_UUID, fullImageName); data.put(ServiceConstants.FIELD_LAUNCH_CONFIG, launchConfig); } } } List<Object> modifiedSlcs = new ArrayList<>(); if (data.get(ServiceConstants.FIELD_SECONDARY_LAUNCH_CONFIGS) != null) { List<Object> slcs = (List<Object>)data.get(ServiceConstants.FIELD_SECONDARY_LAUNCH_CONFIGS); for (Object slcObj : slcs) { Map<String, Object> slc = (Map<String, Object>) slcObj; if (slc.get(InstanceConstants.FIELD_IMAGE_UUID) != null) { Object imageUuid = slc.get(InstanceConstants.FIELD_IMAGE_UUID); if (imageUuid != null && !imageUuid.toString().equalsIgnoreCase(ServiceConstants.IMAGE_NONE)) { String fullImageName = ExternalTemplateInstanceFilter.getImageUuid(imageUuid.toString(), storageService); slc.put(InstanceConstants.FIELD_IMAGE_UUID, fullImageName); } } modifiedSlcs.add(slc); } data.put(ServiceConstants.FIELD_SECONDARY_LAUNCH_CONFIGS, modifiedSlcs); } request.setRequestObject(data); return request; } protected void validateIpsHostName(ApiRequest request) { List<?> externalIps = DataUtils.getFieldFromRequest(request, ServiceConstants.FIELD_EXTERNALIPS, List.class); String hostName = DataUtils.getFieldFromRequest(request, ServiceConstants.FIELD_HOSTNAME, String.class); boolean isExternalIps = externalIps != null && !externalIps.isEmpty(); boolean isHostName = hostName != null && !hostName.isEmpty(); if (isExternalIps && isHostName) { ValidationErrorCodes.throwValidationError(ValidationErrorCodes.INVALID_OPTION, ServiceConstants.FIELD_EXTERNALIPS + " and " + ServiceConstants.FIELD_HOSTNAME + " are mutually exclusive"); } } @Override public Object update(String type, String id, ApiRequest request, ResourceManager next) { Service service = objectManager.loadResource(Service.class, id); validateLaunchConfigs(service, request); validateSelector(request); validateLbConfig(request, type); validateScalePolicy(service, request, true); validatePorts(service, type, request); return super.update(type, id, request, next); } protected void validateLaunchConfigs(Service service, ApiRequest request) { Map<String, Object> data = CollectionUtils.toMap(request.getRequestObject()); Object newName = data.get("name"); String serviceName = newName != null ? newName.toString() : service.getName(); List<Map<String, Object>> launchConfigs = populateLaunchConfigs(service, request); validateLaunchConfigNames(service, serviceName, launchConfigs); validateLaunchConfigsCircularRefs(service, serviceName, launchConfigs); validateLaunchConfigScale(service, request); } protected void validateLaunchConfigScale(Service service, ApiRequest request) { Map<String, Object> data = CollectionUtils.toMap(request.getRequestObject()); Object newLaunchConfig = data.get(ServiceConstants.FIELD_LAUNCH_CONFIG); if (newLaunchConfig == null) { return; } Object launchConfig = DataAccessor.field(service, ServiceConstants.FIELD_LAUNCH_CONFIG, Object.class); if (launchConfig == null) { return; } ServiceDiscoveryUtil.validateScaleSwitch(newLaunchConfig, launchConfig); } @SuppressWarnings("unchecked") protected List<Map<String, Object>> populateLaunchConfigs(Service service, ApiRequest request) { Map<String, Object> data = CollectionUtils.toMap(request.getRequestObject()); List<Map<String, Object>> allLaunchConfigs = new ArrayList<>(); Object primaryLaunchConfig = data.get(ServiceConstants.FIELD_LAUNCH_CONFIG); if (primaryLaunchConfig != null) { // remove the name from launchConfig String primaryName = ((Map<String, String>) primaryLaunchConfig).get("name"); if (primaryName != null) { ((Map<String, String>) primaryLaunchConfig).remove("name"); } allLaunchConfigs.add((Map<String, Object>) primaryLaunchConfig); } Object secondaryLaunchConfigs = data .get(ServiceConstants.FIELD_SECONDARY_LAUNCH_CONFIGS); if (secondaryLaunchConfigs != null) { allLaunchConfigs.addAll((List<Map<String, Object>>) secondaryLaunchConfigs); } return allLaunchConfigs; } protected void validateLaunchConfigsCircularRefs(Service service, String serviceName, List<Map<String, Object>> launchConfigs) { Map<String, Set<String>> launchConfigRefs = populateLaunchConfigRefs(service, serviceName, launchConfigs); for (String launchConfigName : launchConfigRefs.keySet()) { validateLaunchConfigCircularRef(launchConfigName, launchConfigRefs, new HashSet<String>()); } } protected void validateLaunchConfigCircularRef(String launchConfigName, Map<String, Set<String>> launchConfigRefs, Set<String> alreadySeenReferences) { Set<String> myRefs = launchConfigRefs.get(launchConfigName); alreadySeenReferences.add(launchConfigName); for (String myRef : myRefs) { if (!launchConfigRefs.containsKey(myRef)) { ValidationErrorCodes.throwValidationError(ValidationErrorCodes.INVALID_REFERENCE, "LaunchConfigName"); } if (alreadySeenReferences.contains(myRef)) { ValidationErrorCodes.throwValidationError(ValidationErrorCodes.INVALID_REFERENCE, "CircularReference"); } if (!launchConfigRefs.get(myRef).isEmpty()) { validateLaunchConfigCircularRef(myRef, launchConfigRefs, alreadySeenReferences); } } } @SuppressWarnings("unchecked") protected Map<String, Set<String>> populateLaunchConfigRefs(Service service, String serviceName, List<Map<String, Object>> launchConfigs) { Map<String, Set<String>> launchConfigRefs = new HashMap<>(); for (Map<String, Object> launchConfig : launchConfigs) { Object launchConfigName = launchConfig.get("name"); if (launchConfigName == null) { launchConfigName = serviceName; } Set<String> refs = new HashSet<>(); Object networkFromLaunchConfig = launchConfig .get(ServiceConstants.FIELD_NETWORK_LAUNCH_CONFIG); if (networkFromLaunchConfig != null) { refs.add((String) networkFromLaunchConfig); } Object volumesFromLaunchConfigs = launchConfig .get(ServiceConstants.FIELD_DATA_VOLUMES_LAUNCH_CONFIG); if (volumesFromLaunchConfigs != null) { refs.addAll((List<String>) volumesFromLaunchConfigs); } launchConfigRefs.put(launchConfigName.toString(), refs); } return launchConfigRefs; } protected void validateLaunchConfigNames(Service service, String serviceName, List<Map<String, Object>> launchConfigs) { List<String> usedNames = new ArrayList<>(); List<? extends Service> existingSvcs = objectManager.find(Service.class, SERVICE.STACK_ID, service.getStackId(), SERVICE.REMOVED, null); for (Service existingSvc : existingSvcs) { if (existingSvc.getId().equals(service.getId())) { continue; } usedNames.add(existingSvc.getName().toLowerCase()); for (String usedLcName : ServiceDiscoveryUtil.getServiceLaunchConfigNames(existingSvc)) { usedNames.add(usedLcName.toLowerCase()); } } List<String> namesToValidate = new ArrayList<>(); namesToValidate.add(serviceName.toLowerCase()); for (Map<String, Object> launchConfig : launchConfigs) { Object name = launchConfig.get("name"); if (name != null) { namesToValidate.add(name.toString().toLowerCase()); } } for (String name : namesToValidate) { validateName(name.toString()); if (usedNames.contains(name)) { ValidationErrorCodes.throwValidationError(ValidationErrorCodes.NOT_UNIQUE, "name"); } usedNames.add(name.toString().toLowerCase()); } } protected void validateName(String name) { validateDNSPatternForName(name); } }