-
Notifications
You must be signed in to change notification settings - Fork 29k
[SPARK-23529][K8s] Support mounting volumes #21260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
30dd812
36a10ac
9d361ba
5edce1b
9bdbd73
e68d34c
e3f8d9a
beff9a9
517de7c
d960e34
9e4be63
f714b8e
7433244
45eb477
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,11 +16,14 @@ | |
| */ | ||
| package org.apache.spark.deploy.k8s | ||
|
|
||
| import java.util.NoSuchElementException | ||
|
|
||
| import scala.util.{Failure, Success, Try} | ||
|
|
||
| import org.apache.spark.SparkConf | ||
| import org.apache.spark.deploy.k8s.Config._ | ||
|
|
||
| private[spark] object KubernetesVolumeUtils { | ||
|
|
||
| /** | ||
| * Extract Spark volume configuration properties with a given name prefix. | ||
| * | ||
|
|
@@ -30,42 +33,82 @@ private[spark] object KubernetesVolumeUtils { | |
| */ | ||
| def parseVolumesWithPrefix( | ||
| sparkConf: SparkConf, | ||
| prefix: String): Iterable[KubernetesVolumeSpec] = { | ||
| val properties = sparkConf.getAllWithPrefix(prefix) | ||
|
|
||
| val propsByTypeName: Map[(String, String), Array[(String, String)]] = | ||
| properties.flatMap { case (k, v) => | ||
| k.split('.').toList match { | ||
| case tpe :: name :: rest => Some(((tpe, name), (rest.mkString("."), v))) | ||
| case _ => None | ||
| } | ||
| }.groupBy(_._1).mapValues(_.map(_._2)) | ||
| prefix: String): Iterable[Try[KubernetesVolumeSpec[_ <: KubernetesVolumeSpecificConf]]] = { | ||
| val properties = sparkConf.getAllWithPrefix(prefix).toMap | ||
|
|
||
| propsByTypeName.map { case ((tpe, name), props) => | ||
| val propMap = props.toMap | ||
| val mountProps = getAllWithPrefix(propMap, s"$KUBERNETES_VOLUMES_MOUNT_KEY.") | ||
| val options = getAllWithPrefix(propMap, s"$KUBERNETES_VOLUMES_OPTIONS_KEY.") | ||
| getVolumeTypesAndNames(properties).map { case (volumeType, volumeName) => | ||
| val pathKey = s"$volumeType.$volumeName.$KUBERNETES_VOLUMES_MOUNT_PATH_KEY" | ||
| val readOnlyKey = s"$volumeType.$volumeName.$KUBERNETES_VOLUMES_MOUNT_READONLY_KEY" | ||
|
|
||
| KubernetesVolumeSpec( | ||
| volumeName = name, | ||
| volumeType = tpe, | ||
| mountPath = mountProps(KUBERNETES_VOLUMES_PATH_KEY), | ||
| mountReadOnly = mountProps(KUBERNETES_VOLUMES_READONLY_KEY).toBoolean, | ||
| optionsSpec = options | ||
| for { | ||
| path <- properties.getTry(pathKey) | ||
| readOnly <- properties.getTry(readOnlyKey) | ||
|
||
| volumeConf <- parseVolumeSpecificConf(properties, volumeType, volumeName) | ||
| } yield KubernetesVolumeSpec( | ||
| volumeName = volumeName, | ||
| mountPath = path, | ||
| mountReadOnly = readOnly.toBoolean, | ||
| volumeConf = volumeConf | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Extract subset of elements with keys matching on prefix, which is then excluded | ||
| * @param props properties to extract data from | ||
| * @param prefix prefix to match against | ||
| * @return subset of original props | ||
| * Get unique pairs of volumeType and volumeName, | ||
| * assuming options are formatted in this way: | ||
| * `volumeType`.`volumeName`.`property` = `value` | ||
| * @param properties flat mapping of property names to values | ||
| * @return Set[(volumeType, volumeName)] | ||
| */ | ||
| private def getAllWithPrefix(props: Map[String, String], prefix: String): Map[String, String] = { | ||
| props | ||
| .filterKeys(_.startsWith(prefix)) | ||
| .map { case (k, v) => k.substring(prefix.length) -> v } | ||
| private def getVolumeTypesAndNames( | ||
| properties: Map[String, String] | ||
| ): Set[(String, String)] = { | ||
| properties.keys.flatMap { k => | ||
| k.split('.').toList match { | ||
| case tpe :: name :: _ => Some((tpe, name)) | ||
| case _ => None | ||
| } | ||
| }.toSet | ||
| } | ||
|
|
||
| private def parseVolumeSpecificConf( | ||
| options: Map[String, String], | ||
| volumeType: String, | ||
| volumeName: String): Try[KubernetesVolumeSpecificConf] = { | ||
| volumeType match { | ||
| case KUBERNETES_VOLUMES_HOSTPATH_TYPE => | ||
| val pathKey = s"$volumeType.$volumeName.$KUBERNETES_VOLUMES_OPTIONS_PATH_KEY" | ||
| for { | ||
| path <- options.getTry(pathKey) | ||
| } yield KubernetesHostPathVolumeConf(path) | ||
|
|
||
| case KUBERNETES_VOLUMES_PVC_TYPE => | ||
| val claimNameKey = s"$volumeType.$volumeName.$KUBERNETES_VOLUMES_OPTIONS_CLAIM_NAME_KEY" | ||
| for { | ||
| claimName <- options.getTry(claimNameKey) | ||
| } yield KubernetesPVCVolumeConf(claimName) | ||
|
|
||
| case KUBERNETES_VOLUMES_EMPTYDIR_TYPE => | ||
| val mediumKey = s"$volumeType.$volumeName.$KUBERNETES_VOLUMES_OPTIONS_MEDIUM_KEY" | ||
| val sizeLimitKey = s"$volumeType.$volumeName.$KUBERNETES_VOLUMES_OPTIONS_SIZE_LIMIT_KEY" | ||
| for { | ||
| medium <- options.getTry(mediumKey) | ||
| sizeLimit <- options.getTry(sizeLimitKey) | ||
|
||
| } yield KubernetesEmptyDirVolumeConf(medium, sizeLimit) | ||
|
|
||
| case _ => | ||
| Failure(new RuntimeException(s"Kubernetes Volume type `$volumeType` is not supported")) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Convenience wrapper to accumulate key lookup errors | ||
| */ | ||
| implicit private class MapOps[A, B](m: Map[A, B]) { | ||
| def getTry(key: A): Try[B] = { | ||
| m | ||
| .get(key) | ||
| .fold[Try[B]](Failure(new NoSuchElementException(key.toString)))(Success(_)) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,8 +18,7 @@ package org.apache.spark.deploy.k8s.features | |
|
|
||
| import io.fabric8.kubernetes.api.model._ | ||
|
|
||
| import org.apache.spark.deploy.k8s.{KubernetesConf, KubernetesRoleSpecificConf, KubernetesVolumeSpec, SparkPod} | ||
| import org.apache.spark.deploy.k8s.Config._ | ||
| import org.apache.spark.deploy.k8s._ | ||
|
|
||
| private[spark] class MountVolumesFeatureStep( | ||
| kubernetesConf: KubernetesConf[_ <: KubernetesRoleSpecificConf]) | ||
|
||
|
|
@@ -46,36 +45,32 @@ private[spark] class MountVolumesFeatureStep( | |
| override def getAdditionalKubernetesResources(): Seq[HasMetadata] = Seq.empty | ||
|
|
||
| private def constructVolumes( | ||
| volumeSpecs: Iterable[KubernetesVolumeSpec]): Iterable[(VolumeMount, Volume)] = { | ||
| volumeSpecs: Iterable[KubernetesVolumeSpec[_ <: KubernetesVolumeSpecificConf]] | ||
| ): Iterable[(VolumeMount, Volume)] = { | ||
| volumeSpecs.map { spec => | ||
| val volumeMount = new VolumeMountBuilder() | ||
| .withMountPath(spec.mountPath) | ||
| .withReadOnly(spec.mountReadOnly) | ||
| .withName(spec.volumeName) | ||
| .build() | ||
|
|
||
| val volumeBuilder = spec.volumeType match { | ||
| case KUBERNETES_VOLUMES_HOSTPATH_KEY => | ||
| val hostPath = spec.optionsSpec(KUBERNETES_VOLUMES_PATH_KEY) | ||
| val volumeBuilder = spec.volumeConf match { | ||
| case KubernetesHostPathVolumeConf(hostPath) => | ||
| new VolumeBuilder() | ||
| .withHostPath(new HostPathVolumeSource(hostPath)) | ||
|
|
||
| case KUBERNETES_VOLUMES_PVC_KEY => | ||
| val claimName = spec.optionsSpec(KUBERNETES_VOLUMES_CLAIM_NAME_KEY) | ||
| case KubernetesPVCVolumeConf(claimName) => | ||
| new VolumeBuilder() | ||
| .withPersistentVolumeClaim( | ||
| new PersistentVolumeClaimVolumeSource(claimName, spec.mountReadOnly)) | ||
|
|
||
| case KUBERNETES_VOLUMES_EMPTYDIR_KEY => | ||
| val medium = spec.optionsSpec(KUBERNETES_VOLUMES_MEDIUM_KEY) | ||
| val sizeLimit = spec.optionsSpec(KUBERNETES_VOLUMES_SIZE_LIMIT_KEY) | ||
| case KubernetesEmptyDirVolumeConf(medium, sizeLimit) => | ||
| new VolumeBuilder() | ||
| .withEmptyDir(new EmptyDirVolumeSource(medium, new Quantity(sizeLimit))) | ||
| } | ||
|
|
||
| val volume = volumeBuilder.withName(spec.volumeName).build() | ||
|
|
||
|
||
|
|
||
| (volumeMount, volume) | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are missing