Skip to content

Commit e4561e1

Browse files
author
Marcelo Vanzin
committed
[SPARK-25897][K8S] Hook up k8s integration tests to sbt build.
The integration tests can now be run in sbt if the right profile is enabled, using the "test" task under the respective project. This avoids having to fall back to maven to run the tests, which invalidates all your compiled stuff when you go back to sbt, making development way slower than it should. There's also a task to run the tests directly without refreshing the docker images, which is helpful if you just made a change to the submission code which should not affect the code in the images. The sbt tasks currently are not very customizable; there's some very minor things you can set in the sbt shell itself, but otherwise it's hardcoded to run on minikube. I also had to make some slight adjustments to the IT code itself, mostly to remove assumptions about the existing harness. Tested on sbt and maven. Closes apache#22909 from vanzin/SPARK-25897. Authored-by: Marcelo Vanzin <[email protected]> Signed-off-by: Marcelo Vanzin <[email protected]>
1 parent 0a32238 commit e4561e1

File tree

11 files changed

+126
-83
lines changed

11 files changed

+126
-83
lines changed

bin/docker-image-tool.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ do
197197
if ! which minikube 1>/dev/null; then
198198
error "Cannot find minikube."
199199
fi
200+
if ! minikube status 1>/dev/null; then
201+
error "Cannot contact minikube. Make sure it's running."
202+
fi
200203
eval $(minikube docker-env)
201204
;;
202205
esac

project/SparkBuild.scala

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ object SparkBuild extends PomBuild {
374374
// SPARK-14738 - Remove docker tests from main Spark build
375375
// enable(DockerIntegrationTests.settings)(dockerIntegrationTests)
376376

377+
enable(KubernetesIntegrationTests.settings)(kubernetesIntegrationTests)
378+
377379
/**
378380
* Adds the ability to run the spark shell directly from SBT without building an assembly
379381
* jar.
@@ -458,6 +460,65 @@ object DockerIntegrationTests {
458460
)
459461
}
460462

463+
/**
464+
* These settings run a hardcoded configuration of the Kubernetes integration tests using
465+
* minikube. Docker images will have the "dev" tag, and will be overwritten every time the
466+
* integration tests are run. The integration tests are actually bound to the "test" phase,
467+
* so running "test" on this module will run the integration tests.
468+
*
469+
* There are two ways to run the tests:
470+
* - the "tests" task builds docker images and runs the test, so it's a little slow.
471+
* - the "run-its" task just runs the tests on a pre-built set of images.
472+
*
473+
* Note that this does not use the shell scripts that the maven build uses, which are more
474+
* configurable. This is meant as a quick way for developers to run these tests against their
475+
* local changes.
476+
*/
477+
object KubernetesIntegrationTests {
478+
import BuildCommons._
479+
480+
val dockerBuild = TaskKey[Unit]("docker-imgs", "Build the docker images for ITs.")
481+
val runITs = TaskKey[Unit]("run-its", "Only run ITs, skip image build.")
482+
val imageTag = settingKey[String]("Tag to use for images built during the test.")
483+
val namespace = settingKey[String]("Namespace where to run pods.")
484+
485+
// Hack: this variable is used to control whether to build docker images. It's updated by
486+
// the tasks below in a non-obvious way, so that you get the functionality described in
487+
// the scaladoc above.
488+
private var shouldBuildImage = true
489+
490+
lazy val settings = Seq(
491+
imageTag := "dev",
492+
namespace := "default",
493+
dockerBuild := {
494+
if (shouldBuildImage) {
495+
val dockerTool = s"$sparkHome/bin/docker-image-tool.sh"
496+
val cmd = Seq(dockerTool, "-m", "-t", imageTag.value, "build")
497+
val ec = Process(cmd).!
498+
if (ec != 0) {
499+
throw new IllegalStateException(s"Process '${cmd.mkString(" ")}' exited with $ec.")
500+
}
501+
}
502+
shouldBuildImage = true
503+
},
504+
runITs := Def.taskDyn {
505+
shouldBuildImage = false
506+
Def.task {
507+
(test in Test).value
508+
}
509+
}.value,
510+
test in Test := (test in Test).dependsOn(dockerBuild).value,
511+
javaOptions in Test ++= Seq(
512+
"-Dspark.kubernetes.test.deployMode=minikube",
513+
s"-Dspark.kubernetes.test.imageTag=${imageTag.value}",
514+
s"-Dspark.kubernetes.test.namespace=${namespace.value}",
515+
s"-Dspark.kubernetes.test.unpackSparkDir=$sparkHome"
516+
),
517+
// Force packaging before building images, so that the latest code is tested.
518+
dockerBuild := dockerBuild.dependsOn(packageBin in Compile in assembly).value
519+
)
520+
}
521+
461522
/**
462523
* Overrides to work around sbt's dependency resolution being different from Maven's.
463524
*/

resource-managers/kubernetes/integration-tests/pom.xml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,10 @@
155155
<executions>
156156
<execution>
157157
<id>test</id>
158+
<phase>none</phase>
158159
<goals>
159160
<goal>test</goal>
160161
</goals>
161-
<configuration>
162-
<!-- The negative pattern below prevents integration tests such as
163-
KubernetesSuite from running in the test phase. -->
164-
<suffixes>(?&lt;!Suite)</suffixes>
165-
</configuration>
166162
</execution>
167163
<execution>
168164
<id>integration-test</id>

resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/KubernetesSuite.scala

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,34 @@ package org.apache.spark.deploy.k8s.integrationtest
1919
import java.io.File
2020
import java.nio.file.{Path, Paths}
2121
import java.util.UUID
22-
import java.util.regex.Pattern
2322

24-
import com.google.common.io.PatternFilenameFilter
23+
import scala.collection.JavaConverters._
24+
25+
import com.google.common.base.Charsets
26+
import com.google.common.io.Files
2527
import io.fabric8.kubernetes.api.model.Pod
2628
import io.fabric8.kubernetes.client.{KubernetesClientException, Watcher}
2729
import io.fabric8.kubernetes.client.Watcher.Action
2830
import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll, Tag}
2931
import org.scalatest.Matchers
3032
import org.scalatest.concurrent.{Eventually, PatienceConfiguration}
3133
import org.scalatest.time.{Minutes, Seconds, Span}
32-
import scala.collection.JavaConverters._
3334

34-
import org.apache.spark.SparkFunSuite
35-
import org.apache.spark.deploy.k8s.integrationtest.TestConfig._
35+
import org.apache.spark.{SPARK_VERSION, SparkFunSuite}
3636
import org.apache.spark.deploy.k8s.integrationtest.TestConstants._
3737
import org.apache.spark.deploy.k8s.integrationtest.backend.{IntegrationTestBackend, IntegrationTestBackendFactory}
3838
import org.apache.spark.internal.Logging
3939

40-
private[spark] class KubernetesSuite extends SparkFunSuite
40+
class KubernetesSuite extends SparkFunSuite
4141
with BeforeAndAfterAll with BeforeAndAfter with BasicTestsSuite with SecretsTestsSuite
4242
with PythonTestsSuite with ClientModeTestsSuite with PodTemplateSuite
4343
with Logging with Eventually with Matchers {
4444

4545
import KubernetesSuite._
4646

47-
private var sparkHomeDir: Path = _
48-
private var pyImage: String = _
49-
private var rImage: String = _
47+
protected var sparkHomeDir: Path = _
48+
protected var pyImage: String = _
49+
protected var rImage: String = _
5050

5151
protected var image: String = _
5252
protected var testBackend: IntegrationTestBackend = _
@@ -67,6 +67,30 @@ private[spark] class KubernetesSuite extends SparkFunSuite
6767
private val extraExecTotalMemory =
6868
s"${(1024 + memOverheadConstant*1024 + additionalMemory).toInt}Mi"
6969

70+
/**
71+
* Build the image ref for the given image name, taking the repo and tag from the
72+
* test configuration.
73+
*/
74+
private def testImageRef(name: String): String = {
75+
val tag = sys.props.get(CONFIG_KEY_IMAGE_TAG_FILE)
76+
.map { path =>
77+
val tagFile = new File(path)
78+
require(tagFile.isFile,
79+
s"No file found for image tag at ${tagFile.getAbsolutePath}.")
80+
Files.toString(tagFile, Charsets.UTF_8).trim
81+
}
82+
.orElse(sys.props.get(CONFIG_KEY_IMAGE_TAG))
83+
.getOrElse {
84+
throw new IllegalArgumentException(
85+
s"One of $CONFIG_KEY_IMAGE_TAG_FILE or $CONFIG_KEY_IMAGE_TAG is required.")
86+
}
87+
val repo = sys.props.get(CONFIG_KEY_IMAGE_REPO)
88+
.map { _ + "/" }
89+
.getOrElse("")
90+
91+
s"$repo$name:$tag"
92+
}
93+
7094
override def beforeAll(): Unit = {
7195
super.beforeAll()
7296
// The scalatest-maven-plugin gives system properties that are referenced but not set null
@@ -83,17 +107,16 @@ private[spark] class KubernetesSuite extends SparkFunSuite
83107
sparkHomeDir = Paths.get(sparkDirProp)
84108
require(sparkHomeDir.toFile.isDirectory,
85109
s"No directory found for spark home specified at $sparkHomeDir.")
86-
val imageTag = getTestImageTag
87-
val imageRepo = getTestImageRepo
88-
image = s"$imageRepo/spark:$imageTag"
89-
pyImage = s"$imageRepo/spark-py:$imageTag"
90-
rImage = s"$imageRepo/spark-r:$imageTag"
91-
92-
val sparkDistroExamplesJarFile: File = sparkHomeDir.resolve(Paths.get("examples", "jars"))
93-
.toFile
94-
.listFiles(new PatternFilenameFilter(Pattern.compile("^spark-examples_.*\\.jar$")))(0)
95-
containerLocalSparkDistroExamplesJar = s"local:///opt/spark/examples/jars/" +
96-
s"${sparkDistroExamplesJarFile.getName}"
110+
image = testImageRef("spark")
111+
pyImage = testImageRef("spark-py")
112+
rImage = testImageRef("spark-r")
113+
114+
val scalaVersion = scala.util.Properties.versionNumberString
115+
.split("\\.")
116+
.take(2)
117+
.mkString(".")
118+
containerLocalSparkDistroExamplesJar =
119+
s"local:///opt/spark/examples/jars/spark-examples_$scalaVersion-${SPARK_VERSION}.jar"
97120
testBackend = IntegrationTestBackendFactory.getTestBackend
98121
testBackend.initialize()
99122
kubernetesTestComponents = new KubernetesTestComponents(testBackend.getKubernetesClient)

resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/ProcessUtils.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ object ProcessUtils extends Logging {
2828
* executeProcess is used to run a command and return the output if it
2929
* completes within timeout seconds.
3030
*/
31-
def executeProcess(fullCommand: Array[String], timeout: Long, dumpErrors: Boolean = false): Seq[String] = {
31+
def executeProcess(
32+
fullCommand: Array[String],
33+
timeout: Long,
34+
dumpErrors: Boolean = false): Seq[String] = {
3235
val pb = new ProcessBuilder().command(fullCommand: _*)
3336
pb.redirectErrorStream(true)
3437
val proc = pb.start()
@@ -41,7 +44,8 @@ object ProcessUtils extends Logging {
4144
assert(proc.waitFor(timeout, TimeUnit.SECONDS),
4245
s"Timed out while executing ${fullCommand.mkString(" ")}")
4346
assert(proc.exitValue == 0,
44-
s"Failed to execute ${fullCommand.mkString(" ")}${if (dumpErrors) "\n" + outputLines.mkString("\n")}")
47+
s"Failed to execute ${fullCommand.mkString(" ")}" +
48+
s"${if (dumpErrors) "\n" + outputLines.mkString("\n")}")
4549
outputLines
4650
}
4751
}

resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/PythonTestsSuite.scala

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,14 @@
1616
*/
1717
package org.apache.spark.deploy.k8s.integrationtest
1818

19-
import org.apache.spark.deploy.k8s.integrationtest.TestConfig.{getTestImageRepo, getTestImageTag}
20-
2119
private[spark] trait PythonTestsSuite { k8sSuite: KubernetesSuite =>
2220

2321
import PythonTestsSuite._
2422
import KubernetesSuite.k8sTestTag
2523

26-
private val pySparkDockerImage =
27-
s"${getTestImageRepo}/spark-py:${getTestImageTag}"
2824
test("Run PySpark on simple pi.py example", k8sTestTag) {
2925
sparkAppConf
30-
.set("spark.kubernetes.container.image", pySparkDockerImage)
26+
.set("spark.kubernetes.container.image", pyImage)
3127
runSparkApplicationAndVerifyCompletion(
3228
appResource = PYSPARK_PI,
3329
mainClass = "",
@@ -41,7 +37,7 @@ private[spark] trait PythonTestsSuite { k8sSuite: KubernetesSuite =>
4137

4238
test("Run PySpark with Python2 to test a pyfiles example", k8sTestTag) {
4339
sparkAppConf
44-
.set("spark.kubernetes.container.image", pySparkDockerImage)
40+
.set("spark.kubernetes.container.image", pyImage)
4541
.set("spark.kubernetes.pyspark.pythonVersion", "2")
4642
runSparkApplicationAndVerifyCompletion(
4743
appResource = PYSPARK_FILES,
@@ -59,7 +55,7 @@ private[spark] trait PythonTestsSuite { k8sSuite: KubernetesSuite =>
5955

6056
test("Run PySpark with Python3 to test a pyfiles example", k8sTestTag) {
6157
sparkAppConf
62-
.set("spark.kubernetes.container.image", pySparkDockerImage)
58+
.set("spark.kubernetes.container.image", pyImage)
6359
.set("spark.kubernetes.pyspark.pythonVersion", "3")
6460
runSparkApplicationAndVerifyCompletion(
6561
appResource = PYSPARK_FILES,
@@ -77,7 +73,7 @@ private[spark] trait PythonTestsSuite { k8sSuite: KubernetesSuite =>
7773

7874
test("Run PySpark with memory customization", k8sTestTag) {
7975
sparkAppConf
80-
.set("spark.kubernetes.container.image", pySparkDockerImage)
76+
.set("spark.kubernetes.container.image", pyImage)
8177
.set("spark.kubernetes.pyspark.pythonVersion", "3")
8278
.set("spark.kubernetes.memoryOverheadFactor", s"$memOverheadConstant")
8379
.set("spark.executor.pyspark.memory", s"${additionalMemory}m")

resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/RTestsSuite.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@
1616
*/
1717
package org.apache.spark.deploy.k8s.integrationtest
1818

19-
import org.apache.spark.deploy.k8s.integrationtest.TestConfig.{getTestImageRepo, getTestImageTag}
20-
2119
private[spark] trait RTestsSuite { k8sSuite: KubernetesSuite =>
2220

2321
import RTestsSuite._
2422
import KubernetesSuite.k8sTestTag
2523

2624
test("Run SparkR on simple dataframe.R example", k8sTestTag) {
27-
sparkAppConf
28-
.set("spark.kubernetes.container.image", s"${getTestImageRepo}/spark-r:${getTestImageTag}")
25+
sparkAppConf.set("spark.kubernetes.container.image", rImage)
2926
runSparkApplicationAndVerifyCompletion(
3027
appResource = SPARK_R_DATAFRAME_TEST,
3128
mainClass = "",

resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/TestConfig.scala

Lines changed: 0 additions & 40 deletions
This file was deleted.

resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/TestConstants.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ object TestConstants {
2626
val CONFIG_KEY_KUBE_MASTER_URL = "spark.kubernetes.test.master"
2727
val CONFIG_KEY_KUBE_NAMESPACE = "spark.kubernetes.test.namespace"
2828
val CONFIG_KEY_KUBE_SVC_ACCOUNT = "spark.kubernetes.test.serviceAccountName"
29-
val CONFIG_KEY_IMAGE_TAG = "spark.kubernetes.test.imageTagF"
29+
val CONFIG_KEY_IMAGE_TAG = "spark.kubernetes.test.imageTag"
3030
val CONFIG_KEY_IMAGE_TAG_FILE = "spark.kubernetes.test.imageTagFile"
3131
val CONFIG_KEY_IMAGE_REPO = "spark.kubernetes.test.imageRepo"
3232
val CONFIG_KEY_UNPACK_DIR = "spark.kubernetes.test.unpackSparkDir"

resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/backend/IntegrationTestBackend.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.spark.deploy.k8s.integrationtest.backend
1919

2020
import io.fabric8.kubernetes.client.DefaultKubernetesClient
21+
2122
import org.apache.spark.deploy.k8s.integrationtest.TestConstants._
2223
import org.apache.spark.deploy.k8s.integrationtest.backend.cloud.KubeConfigBackend
2324
import org.apache.spark.deploy.k8s.integrationtest.backend.docker.DockerForDesktopBackend
@@ -35,7 +36,8 @@ private[spark] object IntegrationTestBackendFactory {
3536
.getOrElse(BACKEND_MINIKUBE)
3637
deployMode match {
3738
case BACKEND_MINIKUBE => MinikubeTestBackend
38-
case BACKEND_CLOUD => new KubeConfigBackend(System.getProperty(CONFIG_KEY_KUBE_CONFIG_CONTEXT))
39+
case BACKEND_CLOUD =>
40+
new KubeConfigBackend(System.getProperty(CONFIG_KEY_KUBE_CONFIG_CONTEXT))
3941
case BACKEND_DOCKER_FOR_DESKTOP => DockerForDesktopBackend
4042
case _ => throw new IllegalArgumentException("Invalid " +
4143
CONFIG_KEY_DEPLOY_MODE + ": " + deployMode)

0 commit comments

Comments
 (0)