package com.mesosphere.sdk.config.validate; import com.mesosphere.sdk.dcos.Capabilities; import com.mesosphere.sdk.dcos.DcosConstants; import com.mesosphere.sdk.offer.Constants; import com.mesosphere.sdk.offer.LoggingUtils; import com.mesosphere.sdk.specification.NetworkSpec; import com.mesosphere.sdk.specification.PodSpec; import com.mesosphere.sdk.specification.ResourceSpec; import com.mesosphere.sdk.specification.SecretSpec; import com.mesosphere.sdk.specification.ServiceSpec; import com.mesosphere.sdk.specification.TaskSpec; import com.mesosphere.sdk.specification.VolumeSpec; import org.apache.commons.collections.MapUtils; import org.slf4j.Logger; import java.util.ArrayList; import java.util.Collection; import java.util.Optional; /** * This class contains logic for validating that a {@link ServiceSpec} only requires features * supported by the DC/OS cluster being run on. */ public class PodSpecsCannotUseUnsupportedFeatures implements ConfigValidator<ServiceSpec> { private final Logger logger = LoggingUtils.getLogger(getClass(), Optional.empty()); public static boolean serviceRequestsGpuResources(ServiceSpec serviceSpec) { return serviceSpec.getPods().stream().anyMatch( PodSpecsCannotUseUnsupportedFeatures::podRequestsGpuResources ); } private static boolean podRequestsGpuResources(PodSpec podSpec) { // control automatic opt-in to scarce resources (GPUs) here. // If the framework specifies GPU resources >= 1 then we opt-in to scarce resource, otherwise // follow the default policy (which as of 8/3/17 was to opt-out) if (DcosConstants.DEFAULT_GPU_POLICY) { return true; } return podSpec.getTasks().stream() .flatMap(taskSpec -> taskSpec.getResourceSet().getResources().stream()) .anyMatch(resourceSpec -> resourceSpec.getName().equals(Constants.GPUS_RESOURCE_TYPE) && resourceSpec.getValue().getScalar().getValue() >= 1); } private static boolean podRequestsCNI(PodSpec podSpec) { // if we don't have a cluster that supports CNI port mapping make sure that we don't either // (1) specify any networks and/ // (2) if we are make sure we didn't explicitly do any port mapping or that any of the tasks // ask for ports (which will automatically be forwarded, this requiring port mapping). for (NetworkSpec networkSpec : podSpec.getNetworks()) { if (!MapUtils.isEmpty(networkSpec.getPortMappings())) { return true; } } for (TaskSpec taskSpec : podSpec.getTasks()) { for (ResourceSpec resourceSpec : taskSpec.getResourceSet().getResources()) { if (resourceSpec.getName().equals("ports")) { return true; } } } return false; } private static boolean podRequestsEnvBasedSecrets(PodSpec podSpec) { for (SecretSpec secretSpec : podSpec.getSecrets()) { if (secretSpec.getEnvKey().isPresent()) { return true; } } return false; } private static boolean podRequestsFileBasedSecrets(PodSpec podSpec) { for (SecretSpec secretSpec : podSpec.getSecrets()) { // Default is file-based Secret if (secretSpec.getFilePath().isPresent() || !secretSpec.getEnvKey().isPresent()) { return true; } } return false; } private static boolean podRequestsProfileMountVolumes(PodSpec podSpec) { for (VolumeSpec volumeSpec : podSpec.getVolumes()) { if (!volumeSpec.getProfiles().isEmpty()) { return true; } } return false; } //SUPPRESS CHECKSTYLE CyclomaticComplexity @Override public Collection<ConfigValidationError> validate( Optional<ServiceSpec> oldConfig, ServiceSpec newConfig) { Collection<ConfigValidationError> errors = new ArrayList<>(); Capabilities capabilities = Capabilities.getInstance(); boolean supportsPreReservedResources = capabilities.supportsPreReservedResources(); boolean supportsGpus = capabilities.supportsGpuResource(); boolean supportsRLimits = capabilities.supportsRLimits(); boolean supportsCNI = capabilities.supportsCNINetworking(); boolean supportsEnvBasedSecrets = capabilities.supportsEnvBasedSecretsProtobuf(); boolean supportsFileBasedSecrets = capabilities.supportsFileBasedSecrets(); boolean supportsProfileMountVolumes = capabilities.supportsProfileMountVolumes(); boolean supportsSeccomp = capabilities.supportsSeccomp(); boolean supportsShm = capabilities.supportsShm(); for (PodSpec podSpec : newConfig.getPods()) { if (!supportsGpus && podRequestsGpuResources(podSpec)) { errors.add(ConfigValidationError.valueError( // SUPPRESS CHECKSTYLE MultipleStringLiterals "pod:" + podSpec.getType(), Constants.GPUS_RESOURCE_TYPE, "This DC/OS cluster does not support GPU resources")); } if (!supportsPreReservedResources && !podSpec.getPreReservedRole().equals(Constants.ANY_ROLE)) { errors.add(ConfigValidationError.valueError( "pod:" + podSpec.getType(), "pre-reserved-role", "This DC/OS cluster does not support consuming pre-reserved resources.")); } if (!supportsRLimits && !podSpec.getRLimits().isEmpty()) { errors.add(ConfigValidationError.valueError( "pod:" + podSpec.getType(), "rlimits", "This DC/OS cluster does not support setting rlimits")); } if (!supportsCNI && podRequestsCNI(podSpec)) { errors.add(ConfigValidationError.valueError( "pod:" + podSpec.getType(), "network", "This DC/OS cluster does not support CNI port mapping")); } // TODO(MB) : Change validator if we decide to support DCOS_DIRECTIVE label if (!supportsEnvBasedSecrets && podRequestsEnvBasedSecrets(podSpec)) { errors.add(ConfigValidationError.valueError( "pod:" + podSpec.getType(), "secrets:env", "This DC/OS cluster does not support environment-based secrets" )); } if (!supportsFileBasedSecrets && podRequestsFileBasedSecrets(podSpec)) { errors.add(ConfigValidationError.valueError( "pod:" + podSpec.getType(), "secrets:file", "This DC/OS cluster does not support file-based secrets" )); } if (!supportsProfileMountVolumes && podRequestsProfileMountVolumes(podSpec)) { errors.add(ConfigValidationError.valueError( "pod:" + podSpec.getType(), "volumes", "This DC/OS cluster does not support profile mount volumes" )); } if (!supportsSeccomp && ((podSpec.getSeccompUnconfined() != null && podSpec.getSeccompUnconfined()) || podSpec.getSeccompProfileName().isPresent())) { logger.warn("Seccomp is not supported in this cluster."); } if (!supportsShm && (podSpec.getSharedMemory().isPresent() || podSpec.getSharedMemorySize().isPresent())) { errors.add(ConfigValidationError.valueError( "pod:" + podSpec.getType(), "shm", "This DC/OS cluster does not support shared memory" )); } } return errors; } }