Skip to content
This repository was archived by the owner on Oct 24, 2022. It is now read-only.

Commit deae909

Browse files
authored
Add a verifier to check if generated Dockerfiles are up-to-date. (#262)
1 parent e5a679c commit deae909

File tree

3 files changed

+114
-18
lines changed

3 files changed

+114
-18
lines changed

src/main/scala/org/allenai/plugins/DockerBuildPlugin.scala

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ object DockerBuildPlugin extends AutoPlugin {
128128
"The value to use for WORKDIR when generating your Dockerfile. Defaults to \"/stage\"."
129129
)
130130

131+
////////////////////////////////////////////////////////////////////////////////////////////////
132+
// The following settings affect how the plugin behaves, but don't affect file or image output.
133+
////////////////////////////////////////////////////////////////////////////////////////////////
134+
135+
val verifyDockerfileIsStrict: SettingKey[Boolean] = Def.settingKey[Boolean](
136+
"If true, the `verifyDockerfile` task will raise an error if the current Dockerfile does " +
137+
"not match the generated one. Defaults to true."
138+
)
139+
140+
val verifyDockerfileOnBuild: SettingKey[Boolean] = Def.settingKey[Boolean](
141+
"If true, `dockerBuild` will depend on `verifyDockerfile`, and will not build if the " +
142+
"Dockerfile is not up-to-date. Defaults to false."
143+
)
144+
131145
////////////////////////////////////////////////////////////////////////////////////////////////
132146
// The following keys are for generating dockerfiles; and staging, building, running, and
133147
// pushing images. These should not be overridden from the defaults unless you know what you're
@@ -139,6 +153,12 @@ object DockerBuildPlugin extends AutoPlugin {
139153
"`dockerfileLocation`."
140154
)
141155

156+
val verifyDockerfile: TaskKey[Boolean] = Def.taskKey[Boolean](
157+
"Checks if the Dockerfile that would be generated by `generateDockerfile` is the same as " +
158+
"the Dockerfile at `dockerfileLocation`. This will raise an error if " +
159+
"`verifyDockerfileIsStrict` is true, and print a warning otherwise."
160+
)
161+
142162
val dockerDependencyStage: TaskKey[File] = Def.taskKey[File](
143163
"Builds a staged directory under target/docker/dependencies containing project " +
144164
"dependencies. This will include a generated Dockerfile. This returns the staging " +
@@ -301,12 +321,27 @@ object DockerBuildPlugin extends AutoPlugin {
301321
}
302322
}
303323

304-
//////////////////////////////////////////////////////////////////////////////////////////////////
305-
// Definitions for plugin tasks.
306-
//////////////////////////////////////////////////////////////////////////////////////////////////
324+
/** Task to read the current text of the main Dockerfile below the sigil line. This returns None
325+
* if and only if the file exists, but the sigil line cannot be found, since this is considered
326+
* a potential user error.
327+
*/
328+
lazy val customDockerfileContents: Def.Initialize[Task[Option[String]]] = Def.task {
329+
val dockerfile = dockerfileLocation.value
330+
if (dockerfile.exists) {
331+
val lines = IO.readLines(dockerfile)
332+
val remainder = lines.dropWhile(_ != DOCKERFILE_SIGIL)
333+
if (remainder.nonEmpty) {
334+
Some(remainder.tail.mkString("\n") + "\n")
335+
} else {
336+
None
337+
}
338+
} else {
339+
Some("")
340+
}
341+
}
307342

308-
/** Task to build a dockerfile using the current project's sbt settings to populate it. */
309-
lazy val generateDockerfileDef: Def.Initialize[Task[Unit]] = Def.task {
343+
/** Task to build dockerfile contents using the current project's sbt settings and return it. */
344+
lazy val generatedDockerfileContents: Def.Initialize[Task[String]] = Def.task {
310345
val logger = Keys.streams.value.log
311346

312347
// Create the copy commands.
@@ -352,7 +387,7 @@ object DockerBuildPlugin extends AutoPlugin {
352387
// Create the text for dockerMainArgs and CMD command.
353388
val dockerMainArgsText = dockerMainArgs.value.map('"' + _ + '"').mkString(", ")
354389

355-
val dockerfileContents = s"""# AUTOGENERATED
390+
s"""# AUTOGENERATED
356391
# Most lines in this file are derived from sbt settings. These settings are printed above the lines
357392
# they affect.
358393
#
@@ -413,23 +448,52 @@ COPY lib lib
413448
# Do not remove this line unless you want your changes overwritten!
414449
$DOCKERFILE_SIGIL
415450
"""
451+
}
416452

417-
// If there is already a Dockerfile, retain any contents past the sigil.
453+
/** Task to check the current dockerfile contents against the generated contents, returning true
454+
* if they are up-to-date.
455+
*/
456+
lazy val checkDockerfile: Def.Initialize[Task[Boolean]] = Def.task {
457+
val newContents = {
458+
val generatedContents = generatedDockerfileContents.value
459+
val customContents = customDockerfileContents.value.getOrElse("")
460+
generatedContents + customContents
461+
}
418462
val dockerfile = dockerfileLocation.value
419-
val existingContents = if (dockerfile.exists) {
420-
val lines = IO.readLines(dockerfile)
421-
val remainder = lines.dropWhile(_ != DOCKERFILE_SIGIL)
422-
if (remainder.nonEmpty) {
423-
remainder.tail.mkString("\n")
424-
} else {
425-
logger.warn(s"Overwriting Dockerfile at $dockerfile...")
426-
""
427-
}
428-
} else {
463+
val oldContents = if (dockerfile.exists) IO.read(dockerfileLocation.value) else ""
464+
465+
newContents == oldContents
466+
}
467+
468+
//////////////////////////////////////////////////////////////////////////////////////////////////
469+
// Definitions for plugin tasks.
470+
//////////////////////////////////////////////////////////////////////////////////////////////////
471+
472+
/** Task to build a dockerfile using the current project's sbt settings to populate it. */
473+
lazy val generateDockerfileDef: Def.Initialize[Task[Unit]] = Def.task {
474+
val generatedContents = generatedDockerfileContents.value
475+
val customContents = customDockerfileContents.value.getOrElse {
476+
Keys.streams.value.log(s"Overwriting Dockerfile at ${dockerfileLocation.value}...")
429477
""
430478
}
479+
IO.write(dockerfileLocation.value, generatedContents + customContents)
480+
}
431481

432-
IO.write(dockerfile, dockerfileContents + existingContents + "\n")
482+
/** Task verify that the current Dockerfile is up-to-date. Raises an error if
483+
* verifyDockerfileIsStrict is true.
484+
*/
485+
lazy val verifyDockerfileDef: Def.Initialize[Task[Boolean]] = Def.task {
486+
val isUnchanged = checkDockerfile.value
487+
if (!isUnchanged) {
488+
val message = s"Dockerfile ${dockerfileLocation.value} is not up-to-date. Run " +
489+
"the `generateDockerfile` task to fix."
490+
if (verifyDockerfileIsStrict.value) {
491+
sys.error(message)
492+
} else {
493+
Keys.streams.value.log.warn(message)
494+
}
495+
}
496+
isUnchanged
433497
}
434498

435499
/** Task to stage a docker image containing the dependencies of the current project. This is used
@@ -577,6 +641,27 @@ $DOCKERFILE_SIGIL
577641
dockerDependencyBuild.value
578642
dockerMainStage.value
579643

644+
// Verify the dockerfile.
645+
if (verifyDockerfileOnBuild.value) {
646+
val isUnchanged = checkDockerfile.value
647+
// Note that due to sbt dependency semantics (the fact that all dependencies are always run),
648+
// we duplicate the below logic in the verifyDockerfile task def. Otherwise, we couldn't
649+
// control the error behavior via the verifyDockerfileOnBuild flag above.
650+
if (!isUnchanged) {
651+
if (verifyDockerfileIsStrict.value) {
652+
sys.error(
653+
s"Dockerfile ${dockerfileLocation.value} is not up-to-date, stopping build. Run " +
654+
"`generateDockerfile` to update."
655+
)
656+
} else {
657+
Keys.streams.value.log.warn(
658+
s"Dockerfile ${dockerfileLocation.value} is not up-to-date. Run `generateDockerfile` " +
659+
"to update."
660+
)
661+
}
662+
}
663+
}
664+
580665
val logger = Keys.streams.value.log
581666
logger.info(s"Building main image ${mainImageNameSuffix.value}...")
582667

@@ -690,7 +775,10 @@ $DOCKERFILE_SIGIL
690775
},
691776
dockerMainArgs := Seq.empty,
692777
dockerWorkdir := "/stage",
778+
verifyDockerfileIsStrict := true,
779+
verifyDockerfileOnBuild := false,
693780
generateDockerfile := generateDockerfileDef.value,
781+
verifyDockerfile := verifyDockerfileDef.value,
694782
dockerDependencyStage := dependencyStageDef.value,
695783
dockerMainStage := mainImageStageDef.value,
696784
dockerDependencyBuild := dependencyBuildDef.value,

src/sbt-test/sbt-plugins/simple/docker/build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ appendToDockerfile := {
99
val dockerfile = dockerfileLocation.value
1010
IO.append(dockerfile, "# TESTING\n")
1111
}
12+
13+
verifyDockerfileOnBuild := true

src/sbt-test/sbt-plugins/simple/test

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,9 @@ $ exists docker/target/docker/main/lib/docker.docker-0.1-SNAPSHOT.jar
9393

9494
# Test that we can re-run staging without error.
9595
> docker/dockerMainStage
96+
97+
# Test that changes to the Dockerfile are detected.
98+
> docker/verifyDockerfile
99+
# Trigger a change in the file.
100+
> set dockerPorts.in(docker) += 1234
101+
-> docker/verifyDockerfile

0 commit comments

Comments
 (0)