-
Notifications
You must be signed in to change notification settings - Fork 115
Allow adding arbitrary files #71
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,7 +61,9 @@ private[spark] class Client( | |
| private val sslSecretsDirectory = s"$DRIVER_CONTAINER_SECRETS_BASE_DIR/$kubernetesAppId-ssl" | ||
| private val sslSecretsName = s"$SUBMISSION_SSL_SECRETS_PREFIX-$kubernetesAppId" | ||
| private val driverDockerImage = sparkConf.get(DRIVER_DOCKER_IMAGE) | ||
| private val uploadedJars = sparkConf.get(KUBERNETES_DRIVER_UPLOAD_JARS) | ||
| private val uploadedJars = sparkConf.get(KUBERNETES_DRIVER_UPLOAD_JARS).filter(_.nonEmpty) | ||
| private val uploadedFiles = sparkConf.get(KUBERNETES_DRIVER_UPLOAD_FILES).filter(_.nonEmpty) | ||
| validateNoDuplicateUploadFileNames() | ||
| private val uiPort = sparkConf.getInt("spark.ui.port", DEFAULT_UI_PORT) | ||
| private val driverSubmitTimeoutSecs = sparkConf.get(KUBERNETES_DRIVER_SUBMIT_TIMEOUT) | ||
|
|
||
|
|
@@ -511,18 +513,40 @@ private[spark] class Client( | |
| case "container" => ContainerAppResource(appResourceUri.getPath) | ||
| case other => RemoteAppResource(other) | ||
| } | ||
|
|
||
| val uploadJarsBase64Contents = compressJars(uploadedJars) | ||
| val uploadJarsBase64Contents = compressFiles(uploadedJars) | ||
| val uploadFilesBase64Contents = compressFiles(uploadedFiles) | ||
| KubernetesCreateSubmissionRequest( | ||
| appResource = resolvedAppResource, | ||
| mainClass = mainClass, | ||
| appArgs = appArgs, | ||
| secret = secretBase64String, | ||
| sparkProperties = sparkConf.getAll.toMap, | ||
| uploadedJarsBase64Contents = uploadJarsBase64Contents) | ||
| uploadedJarsBase64Contents = uploadJarsBase64Contents, | ||
| uploadedFilesBase64Contents = uploadFilesBase64Contents) | ||
| } | ||
|
|
||
| // Because uploaded files should be added to the working directory of the driver, they | ||
| // need to not have duplicate file names. They are added to the working directory so the | ||
| // user can reliably locate them in their application. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note that this is the same pattern as YARN? I believe it is |
||
| private def validateNoDuplicateUploadFileNames(): Unit = { | ||
| uploadedFiles.foreach { unsplitPaths => | ||
| val splitPaths = unsplitPaths.split(",") | ||
| val allPathsByFileName = splitPaths.groupBy(new File(_).getName) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you're not using |
||
| val pathsWithDuplicateNames = allPathsByFileName.filter(_._2.length > 1) | ||
| if (pathsWithDuplicateNames.nonEmpty) { | ||
| val pathsWithDuplicateNamesSorted = pathsWithDuplicateNames | ||
| .values | ||
| .flatten | ||
| .toList | ||
| .sortBy(new File(_).getName) | ||
| throw new SparkException("Cannot upload files with duplicate names via" + | ||
| s" ${KUBERNETES_DRIVER_UPLOAD_FILES.key}. The following paths have a duplicated" + | ||
| s" file name: ${pathsWithDuplicateNamesSorted.mkString(",")}") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private def compressJars(maybeFilePaths: Option[String]): Option[TarGzippedData] = { | ||
| private def compressFiles(maybeFilePaths: Option[String]): Option[TarGzippedData] = { | ||
| maybeFilePaths | ||
| .map(_.split(",")) | ||
| .map(CompressionUtils.createTarGzip(_)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -94,7 +94,17 @@ package object config { | |
| private[spark] val KUBERNETES_DRIVER_UPLOAD_JARS = | ||
| ConfigBuilder("spark.kubernetes.driver.uploads.jars") | ||
| .doc(""" | ||
| | Comma-separated list of jars to sent to the driver and | ||
| | Comma-separated list of jars to send to the driver and | ||
| | all executors when submitting the application in cluster | ||
| | mode. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and stored in a
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The jars are actually stored in temp space, which makes it basically impossible to find by the user. |
||
| """.stripMargin) | ||
| .stringConf | ||
| .createOptional | ||
|
|
||
| private[spark] val KUBERNETES_DRIVER_UPLOAD_FILES = | ||
| ConfigBuilder("spark.kubernetes.driver.uploads.files") | ||
| .doc(""" | ||
| | Comma-separated list of files to send to the driver and | ||
| | all executors when submitting the application in cluster | ||
| | mode. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note that these are placed in the CWD of the driver and executors once running |
||
| """.stripMargin) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ import org.apache.commons.compress.utils.CharsetNames | |
| import org.apache.commons.io.IOUtils | ||
| import scala.collection.mutable | ||
|
|
||
| import org.apache.spark.SparkException | ||
| import org.apache.spark.deploy.rest.TarGzippedData | ||
| import org.apache.spark.internal.Logging | ||
| import org.apache.spark.util.{ByteBufferOutputStream, Utils} | ||
|
|
@@ -46,7 +47,8 @@ private[spark] object CompressionUtils extends Logging { | |
| * @param paths A list of file paths to be archived | ||
| * @return An in-memory representation of the compressed data. | ||
| */ | ||
| def createTarGzip(paths: Iterable[String]): TarGzippedData = { | ||
| def createTarGzip(paths: Iterable[String]): | ||
| TarGzippedData = { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. format-only change -- is this for style? |
||
| val compressedBytesStream = Utils.tryWithResource(new ByteBufferOutputStream()) { raw => | ||
| Utils.tryWithResource(new GZIPOutputStream(raw)) { gzipping => | ||
| Utils.tryWithResource(new TarArchiveOutputStream( | ||
|
|
@@ -68,8 +70,8 @@ private[spark] object CompressionUtils extends Logging { | |
| while (usedFileNames.contains(resolvedFileName)) { | ||
| val oldResolvedFileName = resolvedFileName | ||
| resolvedFileName = s"$nameWithoutExtension-$deduplicationCounter.$extension" | ||
| logWarning(s"File with name $oldResolvedFileName already exists. Trying to add with" + | ||
| s" file name $resolvedFileName instead.") | ||
| logWarning(s"File with name $oldResolvedFileName already exists. Trying to add" + | ||
| s" with file name $resolvedFileName instead.") | ||
| deduplicationCounter += 1 | ||
| } | ||
| usedFileNames += resolvedFileName | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ package org.apache.spark.deploy.rest.kubernetes | |
|
|
||
| import java.io.File | ||
| import java.net.URI | ||
| import java.nio.file.Paths | ||
| import java.util.concurrent.CountDownLatch | ||
| import javax.servlet.http.{HttpServletRequest, HttpServletResponse} | ||
|
|
||
|
|
@@ -27,7 +28,7 @@ import org.apache.commons.codec.binary.Base64 | |
| import scala.collection.mutable | ||
| import scala.collection.mutable.ArrayBuffer | ||
|
|
||
| import org.apache.spark.{SecurityManager, SPARK_VERSION => sparkVersion, SparkConf, SparkException, SSLOptions} | ||
| import org.apache.spark.{SecurityManager, SPARK_VERSION => sparkVersion, SparkConf, SSLOptions} | ||
| import org.apache.spark.deploy.SparkHadoopUtil | ||
| import org.apache.spark.deploy.rest._ | ||
| import org.apache.spark.util.{ShutdownHookManager, ThreadUtils, Utils} | ||
|
|
@@ -149,37 +150,42 @@ private[spark] class KubernetesSparkRestServer( | |
| appArgs, | ||
| sparkProperties, | ||
| secret, | ||
| uploadedJars) => | ||
| uploadedJars, | ||
| uploadedFiles) => | ||
| val decodedSecret = Base64.decodeBase64(secret) | ||
| if (!expectedApplicationSecret.sameElements(decodedSecret)) { | ||
| responseServlet.setStatus(HttpServletResponse.SC_UNAUTHORIZED) | ||
| handleError("Unauthorized to submit application.") | ||
| } else { | ||
| val tempDir = Utils.createTempDir() | ||
| val appResourcePath = resolvedAppResource(appResource, tempDir) | ||
| val jarsDirectory = new File(tempDir, "jars") | ||
| if (!jarsDirectory.mkdir) { | ||
| throw new IllegalStateException("Failed to create jars dir at" + | ||
| s"${jarsDirectory.getAbsolutePath}") | ||
| } | ||
| val writtenJars = writeBase64ContentsToFiles(uploadedJars, jarsDirectory) | ||
| val driverExtraClasspath = sparkProperties | ||
| .get("spark.driver.extraClassPath") | ||
| .map(_.split(",")) | ||
| .getOrElse(Array.empty[String]) | ||
| val writtenJars = writeUploadedJars(uploadedJars, tempDir) | ||
| val writtenFiles = writeUploadedFiles(uploadedFiles) | ||
| val resolvedSparkProperties = new mutable.HashMap[String, String] | ||
| resolvedSparkProperties ++= sparkProperties | ||
|
|
||
| // Resolve driver classpath and jars | ||
| val originalJars = sparkProperties.get("spark.jars") | ||
| .map(_.split(",")) | ||
| .getOrElse(Array.empty[String]) | ||
| val resolvedJars = writtenJars ++ originalJars ++ Array(appResourcePath) | ||
| val sparkJars = new File(sparkHome, "jars").listFiles().map(_.getAbsolutePath) | ||
| val driverExtraClasspath = sparkProperties | ||
| .get("spark.driver.extraClassPath") | ||
| .map(_.split(",")) | ||
| .getOrElse(Array.empty[String]) | ||
| val driverClasspath = driverExtraClasspath ++ | ||
| resolvedJars ++ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this line removed? should it not have been there before?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually never mind, misread the diff. |
||
| sparkJars ++ | ||
| Array(appResourcePath) | ||
| val resolvedSparkProperties = new mutable.HashMap[String, String] | ||
| resolvedSparkProperties ++= sparkProperties | ||
| sparkJars | ||
| resolvedSparkProperties("spark.jars") = resolvedJars.mkString(",") | ||
|
|
||
| // Resolve spark.files | ||
| val originalFiles = sparkProperties.get("spark.files") | ||
| .map(_.split(",")) | ||
| .getOrElse(Array.empty[String]) | ||
| val resolvedFiles = originalFiles ++ writtenFiles | ||
| resolvedSparkProperties("spark.files") = resolvedFiles.mkString | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this need |
||
|
|
||
| val command = new ArrayBuffer[String] | ||
| command += javaExecutable | ||
| command += "-cp" | ||
|
|
@@ -229,6 +235,21 @@ private[spark] class KubernetesSparkRestServer( | |
| } | ||
| } | ||
|
|
||
| private def writeUploadedJars(files: Option[TarGzippedData], rootTempDir: File): | ||
| Seq[String] = { | ||
| val resolvedDirectory = new File(rootTempDir, "jars") | ||
| if (!resolvedDirectory.mkdir()) { | ||
| throw new IllegalStateException(s"Failed to create jars dir at " + | ||
| resolvedDirectory.getAbsolutePath) | ||
| } | ||
| writeBase64ContentsToFiles(files, resolvedDirectory) | ||
| } | ||
|
|
||
| private def writeUploadedFiles(files: Option[TarGzippedData]): Seq[String] = { | ||
| val workingDir = Paths.get("").toFile.getAbsoluteFile | ||
| writeBase64ContentsToFiles(files, workingDir) | ||
| } | ||
|
|
||
| def resolvedAppResource(appResource: AppResource, tempDir: File): String = { | ||
| val appResourcePath = appResource match { | ||
| case UploadedAppResource(resourceContentsBase64, resourceName) => | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.spark.deploy.kubernetes.integrationtest.jobs | ||
|
|
||
| import java.nio.file.Paths | ||
|
|
||
| import com.google.common.base.Charsets | ||
| import com.google.common.io.Files | ||
|
|
||
| import org.apache.spark.SparkException | ||
| import org.apache.spark.sql.SparkSession | ||
|
|
||
| private[spark] object FileExistenceTest { | ||
|
|
||
| def main(args: Array[String]): Unit = { | ||
| if (args.length < 2) { | ||
| throw new IllegalArgumentException("Usage: WordCount <source-file> <expected contents>") | ||
| } | ||
| // Can't use SparkContext.textFile since the file is local to the driver | ||
| val file = Paths.get(args(0)).toFile | ||
| if (!file.exists()) { | ||
| throw new SparkException(s"Failed to find file at ${file.getAbsolutePath}") | ||
| } else { | ||
| // scalastyle:off println | ||
| val contents = Files.toString(file, Charsets.UTF_8) | ||
| if (args(1) != contents) { | ||
| throw new SparkException(s"Contents do not match. Expected: ${args(1)}," + | ||
| s" actual, $contents") | ||
| } else { | ||
| println(s"File found at ${file.getAbsolutePath} with correct contents.") | ||
| } | ||
| // scalastyle:on println | ||
| } | ||
| val spark = SparkSession.builder() | ||
| .appName("Test") | ||
| .getOrCreate() | ||
| spark.stop() | ||
| } | ||
|
|
||
| } |
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.
this seems a little weird -- should we have that method take an input and return an output instead of relying on the state of the instance vars being set already?