Skip to content
This repository has been archived by the owner on Apr 29, 2024. It is now read-only.

Support docker-compose variable substitution #17

Merged
merged 4 commits into from
Jun 14, 2016
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ plugin will attempt to locate it in one of three places with the precedence orde
testTagsToExecute =: // Set of ScalaTest Tags to execute when dockerComposeTest is run. Separate multiple tags by a comma. It defaults to executing all tests.
testDependenciesClasspath =: // The path to all managed and unmanaged Test and Compile dependencies. This path needs to include the ScalaTest Jar for the tests to execute. This defaults to all managedClasspath and unmanagedClasspath in the Test and fullClasspath in the Compile Scope.
testCasesJar =: // The path to the Jar file containing the tests to execute. This defaults to the Jar file with the tests from the current sbt project.
variablesForSubstitution =: // A Map[String,String] of variables to substitute in your docker-compose file. These are substituted substituted by the plugin and not using environment variables.

There are several sample projects showing how to configure sbt-docker-compose that can be found in the [**examples**] (examples) folder.
Copy link
Contributor

@kurtkopchik kurtkopchik Jun 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under the "Examples" section of the README you should add a "#5" to describe how this example demonstrates the usage of the variablesForSubstitution setting.


Expand Down
11 changes: 11 additions & 0 deletions examples/basic-variable-substitution/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name := "basic"

version := "1.0.0"

scalaVersion := "2.10.6"

enablePlugins(JavaAppPackaging, DockerComposePlugin)

dockerImageCreationTask := (publishLocal in Docker).value

variablesForSubstitution := Map("SOURCE_PORT" -> "5555")
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
basic:
image: basic:1.0.0
environment:
JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
ports:
- "${SOURCE_PORT}:5005"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.11
3 changes: 3 additions & 0 deletions examples/basic-variable-substitution/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
addSbtPlugin("com.typesafe.sbt" %% "sbt-native-packager" % "1.1.0")

addSbtPlugin("com.tapad" % "sbt-docker-compose" % "1.0.8")
14 changes: 14 additions & 0 deletions examples/basic-variable-substitution/src/main/scala/BasicApp.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import scala.Console._
import scala.concurrent.duration._

object BasicApp extends App {
println("Application started....")

val deadline = 1.hour.fromNow
do {
println(s"Running application. Seconds left until showdown: ${deadline.timeLeft.toSeconds}")
Thread.sleep(1000)
} while (deadline.hasTimeLeft())

println("Application shutting down....")
}
24 changes: 17 additions & 7 deletions src/main/scala/com/tapad/docker/ComposeFile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,25 @@ trait ComposeFile extends SettingsHelper with ComposeCustomTagHelpers {
}
}

def readComposeFile(composePath: String): yamlData = {
val fileReader = fromFile(composePath).reader()
try {
new Yaml().load(fileReader).asInstanceOf[java.util.Map[String, java.util.LinkedHashMap[String, Any]]].asScala.toMap
} finally {
fileReader.close()
}
def readComposeFile(composePath: String, variables: Vector[(String, String)] = Vector.empty): yamlData = {
val yamlString = fromFile(composePath).getLines().mkString("\n")
val yamlUpdated = processVariableSubstitution(yamlString, variables)

new Yaml().load(yamlUpdated).asInstanceOf[java.util.Map[String, java.util.LinkedHashMap[String, Any]]].asScala.toMap
}

/**
* Substitute all docker-compose variables in the YAML file. This is traditionally done by docker-compose itself,
* but is being performed by the plugin to support other functionality.
* @param yamlString Stringified docker-compose file.
* @param variables Substitution variables.
* @return An updated stringified docker-compile file.
*/
def processVariableSubstitution(yamlString: String, variables: Vector[(String, String)]) =
variables.foldLeft(yamlString) {
case (y, (key, value)) => y.replaceAll("\\$\\{" + key + "\\}", value)
}

def deleteComposeFile(composePath: String): Boolean = {
Try(new File(composePath).delete()) match {
case Success(i) => true
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/com/tapad/docker/DockerComposeKeys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ trait DockerComposeKeysLocal {
val testCasesJar = settingKey[String]("The path to the Jar file containing the tests to execute. This defaults to the Jar file with the tests from the current sbt project.")
val testDependenciesClasspath = taskKey[String]("The path to all managed and unmanaged Test and Compile dependencies. This path needs to include the ScalaTest Jar for the tests to execute. This defaults to all managedClasspath and unmanagedClasspath in the Test and fullClasspath in the Compile Scope.")
val runningInstances = AttributeKey[List[RunningInstanceInfo]]("For Internal Use: Contains information on the set of running Docker Compose instances.")
val variablesForSubstitution = settingKey[Map[String, String]]("Specify a Map of String to String that will be passed as environment variables to docker-compose and can be used for variable substitution.")
}
23 changes: 14 additions & 9 deletions src/main/scala/com/tapad/docker/DockerComposePlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ case class ServiceInfo(serviceName: String, imageName: String, imageSource: Stri
* with an SBT project.
* @param composeFilePath The path to the Docker Compose file used by this instance
* @param servicesInfo The collection of ServiceInfo objects that define this instance
* @param variables A collection of key value pairs used for docker-compose variable substitution
* @param instanceData An optional parameter to specify additional information about the instance
*/
case class RunningInstanceInfo(instanceName: String, composeServiceName: String, composeFilePath: String,
servicesInfo: Iterable[ServiceInfo], instanceData: Option[Any] = None)
servicesInfo: Iterable[ServiceInfo], variables: Vector[(String, String)] = Vector.empty,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should now be able to remove the previously added variables property and comment from RunningInstanceInfo.

instanceData: Option[Any] = None)

object DockerComposePlugin extends DockerComposePluginLocal {
override def projectSettings = DockerComposeSettings.baseDockerComposeSettings
Expand All @@ -65,6 +67,7 @@ object DockerComposePlugin extends DockerComposePluginLocal {
val testTagsToExecute = DockerComposeKeys.testTagsToExecute
val testCasesJar = DockerComposeKeys.testCasesJar
val scalaTestJar = DockerComposeKeys.testDependenciesClasspath
val variablesForSubstitution = DockerComposeKeys.variablesForSubstitution
}
}

Expand Down Expand Up @@ -148,11 +151,12 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo
*/
def startDockerCompose(implicit state: State, args: Seq[String]): (State, String) = {
val composeFilePath = getSetting(composeFile)
val variables = getSetting(variablesForSubstitution).toVector

printBold(s"Creating Local Docker Compose Environment.")
printBold(s"Reading Compose File: $composeFilePath")

val composeYaml = readComposeFile(composeFilePath)
val composeYaml = readComposeFile(composeFilePath, variables)
val servicesInfo = processCustomTags(state, args, composeYaml)
val updatedComposePath = saveComposeFile(composeYaml)
println(s"Created Compose File with Processed Custom Tags: $updatedComposePath")
Expand All @@ -165,12 +169,12 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo

val newState = Try {
dockerComposeUp(instanceName, updatedComposePath)
val newInstance = getRunningInstanceInfo(state, instanceName, updatedComposePath, servicesInfo)
val newInstance = getRunningInstanceInfo(state, instanceName, updatedComposePath, servicesInfo, variables)

printMappedPortInformation(state, newInstance, dockerComposeVersion)
saveInstanceToSbtSession(state, newInstance)
} getOrElse {
stopLocalDockerInstance(state, instanceName, updatedComposePath)
stopLocalDockerInstance(state, instanceName, updatedComposePath, variables)
throw new IllegalStateException(s"Error starting Docker Compose instance. Shutting down containers...")
}

Expand All @@ -180,14 +184,14 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo
}

def getRunningInstanceInfo(implicit state: State, instanceName: String, composePath: String,
servicesInfo: Iterable[ServiceInfo]): RunningInstanceInfo = {
servicesInfo: Iterable[ServiceInfo], variables: Vector[(String, String)]): RunningInstanceInfo = {
val composeService = getSetting(composeServiceName).toLowerCase
val composeStartTimeout = getSetting(composeContainerStartTimeoutSeconds)
val dockerMachine = getSetting(dockerMachineName)

val serviceInfo = populateServiceInfoForInstance(instanceName, dockerMachine, servicesInfo, composeStartTimeout)

RunningInstanceInfo(instanceName, composeService, composePath, serviceInfo)
RunningInstanceInfo(instanceName, composeService, composePath, serviceInfo, variables)
}

def pullDockerImages(args: Seq[String], services: Iterable[ServiceInfo]): Unit = {
Expand Down Expand Up @@ -220,7 +224,7 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo
//Remove all of the stopped instances from the list
removeList.foreach { instance =>
printBold(s"Stopping and removing local Docker instance: ${instance.instanceName}")
stopLocalDockerInstance(state, instance.instanceName, instance.composeFilePath)
stopLocalDockerInstance(state, instance.instanceName, instance.composeFilePath, instance.variables)
}

if (removeList.isEmpty)
Expand All @@ -241,7 +245,8 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo
updatedState
}

def stopLocalDockerInstance(implicit state: State, instanceName: String, composePath: String): Unit = {
def stopLocalDockerInstance(implicit state: State, instanceName: String, composePath: String,
variables: Vector[(String, String)]): Unit = {
dockerComposeStopInstance(instanceName, composePath)

if (getSetting(composeRemoveContainersOnShutdown)) {
Expand All @@ -252,7 +257,7 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo
// If the compose file being used is a version that creates a new network on startup then remove that network on
// shutdown
if (new File(composePath).exists()) {
val composeYaml = readComposeFile(composePath)
val composeYaml = readComposeFile(composePath, variables)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this call to readComposeFile only is to get the Version information from the compose file it doesn't need to actually substitute any variables. (I think its actually using the already variable substituted version of the compose file in this call already) I was thinking that in this case the variables parameter should not be passed and the default Vector.empty value will be used. You just need to make sure that the code in readComposeFile will gracefully skip the substitution when Vector.empty is used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After that we should be able to also remove the passing of variables to stopLocalDockerInstance and getRunningInstanceInfo

if (getComposeVersion(composeYaml) >= 2) {
val dockerMachine = getSetting(dockerMachineName)
dockerRemoveNetwork(instanceName, dockerMachine)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ trait DockerComposeSettingsLocal extends PrintFormatting {
(fullClasspathCompile.files ++ classpathTestManaged.files ++ classpathTestUnmanaged.files).map(_.getAbsoluteFile).mkString(":")
},
testCasesJar := artifactPath.in(Test, packageBin).value.getAbsolutePath,
variablesForSubstitution := Map[String, String](),
commands ++= Seq(dockerComposeUpCommand, dockerComposeStopCommand, dockerComposeInstancesCommand, dockerComposeTest)
)
}
4 changes: 4 additions & 0 deletions src/test/resources/variable_substitution.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
testservice:
image: testservice:0.0.1
ports:
- "${SOURCE_PORT}:5005"
11 changes: 11 additions & 0 deletions src/test/scala/ComposeFileProcessingSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,17 @@ class ComposeFileProcessingSpec extends FunSuite with BeforeAndAfter with OneIns
}
}

test("Validate that docker-compose variables are substituted") {
val composeMock = getComposeFileMock()
val composeFilePath = getClass.getResource("variable_substitution.yml").getPath
doReturn(composeFilePath).when(composeMock).getSetting(composeFile)(null)

val composeYaml = composeMock.readComposeFile(composeFilePath, Vector(("SOURCE_PORT", "5555")))

val ports = composeYaml.get("testservice").get.get("ports").asInstanceOf[util.ArrayList[String]].get(0)
assert(ports == "5555:5005")
}

def getComposeFileMock(serviceName: String = "testservice", versionNumber: String = "1.0.0", noBuild: Boolean = false): ComposeFile = {
val composeMock = spy(new DockerComposePluginLocal)

Expand Down