From 01a287ea3bad8d3166a473951602b3b81d0d585e Mon Sep 17 00:00:00 2001 From: seglo Date: Fri, 10 Jun 2016 22:36:01 -0400 Subject: [PATCH 1/4] Substitution variable support for docker-compose yaml files. Impl. variablesForSubstitution setting. --- .../basic-variable-substitution/build.sbt | 11 ++++++ .../docker/docker-compose.yml | 6 +++ .../project/build.properties | 1 + .../project/plugins.sbt | 3 ++ .../src/main/scala/BasicApp.scala | 14 +++++++ .../com/tapad/docker/DockerCommands.scala | 15 +++++--- .../com/tapad/docker/DockerComposeKeys.scala | 1 + .../tapad/docker/DockerComposePlugin.scala | 26 ++++++++----- .../tapad/docker/DockerComposeSettings.scala | 1 + src/test/scala/InstanceStoppingSpec.scala | 23 ++++++------ src/test/scala/MockHelpers.scala | 5 ++- src/test/scala/VariableSubstitutionSpec.scala | 37 +++++++++++++++++++ 12 files changed, 114 insertions(+), 29 deletions(-) create mode 100644 examples/basic-variable-substitution/build.sbt create mode 100644 examples/basic-variable-substitution/docker/docker-compose.yml create mode 100644 examples/basic-variable-substitution/project/build.properties create mode 100644 examples/basic-variable-substitution/project/plugins.sbt create mode 100644 examples/basic-variable-substitution/src/main/scala/BasicApp.scala create mode 100644 src/test/scala/VariableSubstitutionSpec.scala diff --git a/examples/basic-variable-substitution/build.sbt b/examples/basic-variable-substitution/build.sbt new file mode 100644 index 0000000..b7f30b4 --- /dev/null +++ b/examples/basic-variable-substitution/build.sbt @@ -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") \ No newline at end of file diff --git a/examples/basic-variable-substitution/docker/docker-compose.yml b/examples/basic-variable-substitution/docker/docker-compose.yml new file mode 100644 index 0000000..f1669f7 --- /dev/null +++ b/examples/basic-variable-substitution/docker/docker-compose.yml @@ -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" \ No newline at end of file diff --git a/examples/basic-variable-substitution/project/build.properties b/examples/basic-variable-substitution/project/build.properties new file mode 100644 index 0000000..59e7c05 --- /dev/null +++ b/examples/basic-variable-substitution/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.11 \ No newline at end of file diff --git a/examples/basic-variable-substitution/project/plugins.sbt b/examples/basic-variable-substitution/project/plugins.sbt new file mode 100644 index 0000000..f00a524 --- /dev/null +++ b/examples/basic-variable-substitution/project/plugins.sbt @@ -0,0 +1,3 @@ +addSbtPlugin("com.typesafe.sbt" %% "sbt-native-packager" % "1.1.0") + +addSbtPlugin("com.tapad" % "sbt-docker-compose" % "1.0.8") \ No newline at end of file diff --git a/examples/basic-variable-substitution/src/main/scala/BasicApp.scala b/examples/basic-variable-substitution/src/main/scala/BasicApp.scala new file mode 100644 index 0000000..67bc23c --- /dev/null +++ b/examples/basic-variable-substitution/src/main/scala/BasicApp.scala @@ -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....") +} \ No newline at end of file diff --git a/src/main/scala/com/tapad/docker/DockerCommands.scala b/src/main/scala/com/tapad/docker/DockerCommands.scala index 6397e44..9a6c3ca 100644 --- a/src/main/scala/com/tapad/docker/DockerCommands.scala +++ b/src/main/scala/com/tapad/docker/DockerCommands.scala @@ -2,18 +2,21 @@ package com.tapad.docker import sbt._ import com.tapad.docker.DockerComposeKeys._ +import sys.process.Process trait DockerCommands { - def dockerComposeUp(instanceName: String, composePath: String): Unit = { - s"docker-compose -p $instanceName -f $composePath up -d".! + def dockerComposeUp(instanceName: String, composePath: String, variables: Vector[(String, String)]): Unit = { + Process(s"docker-compose -p $instanceName -f $composePath up -d", None, variables: _*).run(false) } - def dockerComposeStopInstance(instanceName: String, composePath: String): Unit = { - s"docker-compose -p $instanceName -f $composePath stop".! + def dockerComposeStopInstance(instanceName: String, composePath: String, + variables: Vector[(String, String)] = Vector.empty): Unit = { + Process(s"docker-compose -p $instanceName -f $composePath stop", None, variables: _*).run(false) } - def dockerComposeRemoveContainers(instanceName: String, composePath: String): Unit = { - s"docker-compose -p $instanceName -f $composePath rm -v -f".! + def dockerComposeRemoveContainers(instanceName: String, composePath: String, + variables: Vector[(String, String)] = Vector.empty): Unit = { + Process(s"docker-compose -p $instanceName -f $composePath rm -v -f", None, variables: _*).run(false) } def getDockerComposeVersion: Version = { diff --git a/src/main/scala/com/tapad/docker/DockerComposeKeys.scala b/src/main/scala/com/tapad/docker/DockerComposeKeys.scala index 4bea66e..9acbb58 100644 --- a/src/main/scala/com/tapad/docker/DockerComposeKeys.scala +++ b/src/main/scala/com/tapad/docker/DockerComposeKeys.scala @@ -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.") } \ No newline at end of file diff --git a/src/main/scala/com/tapad/docker/DockerComposePlugin.scala b/src/main/scala/com/tapad/docker/DockerComposePlugin.scala index 442e1a7..17355d4 100644 --- a/src/main/scala/com/tapad/docker/DockerComposePlugin.scala +++ b/src/main/scala/com/tapad/docker/DockerComposePlugin.scala @@ -42,10 +42,13 @@ 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 passed as environment variables to docker-compose for 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, + instanceData: Option[Any] = None) object DockerComposePlugin extends DockerComposePluginLocal { override def projectSettings = DockerComposeSettings.baseDockerComposeSettings @@ -65,6 +68,7 @@ object DockerComposePlugin extends DockerComposePluginLocal { val testTagsToExecute = DockerComposeKeys.testTagsToExecute val testCasesJar = DockerComposeKeys.testCasesJar val scalaTestJar = DockerComposeKeys.testDependenciesClasspath + val variablesForSubstitution = DockerComposeKeys.variablesForSubstitution } } @@ -148,6 +152,7 @@ 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") @@ -164,13 +169,13 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo val instanceName = generateInstanceName(state) val newState = Try { - dockerComposeUp(instanceName, updatedComposePath) - val newInstance = getRunningInstanceInfo(state, instanceName, updatedComposePath, servicesInfo) + dockerComposeUp(instanceName, updatedComposePath, variables) + 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...") } @@ -180,14 +185,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 = { @@ -220,7 +225,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) @@ -241,11 +246,12 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo updatedState } - def stopLocalDockerInstance(implicit state: State, instanceName: String, composePath: String): Unit = { - dockerComposeStopInstance(instanceName, composePath) + def stopLocalDockerInstance(implicit state: State, instanceName: String, composePath: String, + variables: Vector[(String, String)]): Unit = { + dockerComposeStopInstance(instanceName, composePath, variables) if (getSetting(composeRemoveContainersOnShutdown)) { - dockerComposeRemoveContainers(instanceName, composePath) + dockerComposeRemoveContainers(instanceName, composePath, variables) } if (getSetting(composeRemoveNetworkOnShutdown)) { diff --git a/src/main/scala/com/tapad/docker/DockerComposeSettings.scala b/src/main/scala/com/tapad/docker/DockerComposeSettings.scala index dae4694..7d42517 100644 --- a/src/main/scala/com/tapad/docker/DockerComposeSettings.scala +++ b/src/main/scala/com/tapad/docker/DockerComposeSettings.scala @@ -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) ) } \ No newline at end of file diff --git a/src/test/scala/InstanceStoppingSpec.scala b/src/test/scala/InstanceStoppingSpec.scala index b16f3cb..b6e7e95 100644 --- a/src/test/scala/InstanceStoppingSpec.scala +++ b/src/test/scala/InstanceStoppingSpec.scala @@ -8,8 +8,9 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance val instanceId = "instanceId" val composePath = "path" val serviceName = "service" + val variables = Vector[(String, String)](("foo", "bar")) val composeMock = spy(new DockerComposePluginLocal) - val instance = RunningInstanceInfo(instanceId, serviceName, composePath, List.empty) + val instance = RunningInstanceInfo(instanceId, serviceName, composePath, List.empty, variables) mockDockerCommandCalls(composeMock) mockSystemSettings(composeMock, serviceName, Some(List(instance))) @@ -17,8 +18,8 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance composeMock.stopRunningInstances(null, Seq.empty) //Validate that the instance was stopped and cleaned up - verify(composeMock, times(1)).dockerComposeStopInstance(instanceId, composePath) - verify(composeMock, times(1)).dockerComposeRemoveContainers(instanceId, composePath) + verify(composeMock, times(1)).dockerComposeStopInstance(instanceId, composePath, variables) + verify(composeMock, times(1)).dockerComposeRemoveContainers(instanceId, composePath, variables) } test("Validate the proper stopping of a multiple instances when no specific instances are passed in as arguments") { @@ -35,8 +36,8 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance composeMock.stopRunningInstances(null, Seq.empty) //Validate that the instance was stopped and cleaned up - verify(composeMock, times(2)).dockerComposeStopInstance(anyString, anyString) - verify(composeMock, times(2)).dockerComposeRemoveContainers(anyString, anyString) + verify(composeMock, times(2)).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) + verify(composeMock, times(2)).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) } test("Validate the proper stopping of a single instance when multiple instances are running") { @@ -55,8 +56,8 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance //Validate that only once instance was Stopped and Removed verify(composeMock, times(1)).setAttribute(any, any)(any[sbt.State]) - verify(composeMock, times(1)).dockerComposeStopInstance(anyString, anyString) - verify(composeMock, times(1)).dockerComposeRemoveContainers(anyString, anyString) + verify(composeMock, times(1)).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) + verify(composeMock, times(1)).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) } test("Validate that only instances from the current SBT project are stopped when no arguments are supplied to DockerComposeStop") { @@ -73,8 +74,8 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance //Validate that only once instance was Stopped and Removed verify(composeMock, times(1)).setAttribute(any, any)(any[sbt.State]) - verify(composeMock, times(2)).dockerComposeStopInstance(anyString, anyString) - verify(composeMock, times(2)).dockerComposeRemoveContainers(anyString, anyString) + verify(composeMock, times(2)).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) + verify(composeMock, times(2)).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) } test("Validate that instances from any SBT project can be stopped when explicitly passed to DockerComposeStop") { @@ -91,7 +92,7 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance //Validate that only once instance was Stopped and Removed verify(composeMock, times(1)).setAttribute(any, any)(any[sbt.State]) - verify(composeMock, times(1)).dockerComposeStopInstance(anyString, anyString) - verify(composeMock, times(1)).dockerComposeRemoveContainers(anyString, anyString) + verify(composeMock, times(1)).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) + verify(composeMock, times(1)).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) } } diff --git a/src/test/scala/MockHelpers.scala b/src/test/scala/MockHelpers.scala index 16f44b4..a7e8f45 100644 --- a/src/test/scala/MockHelpers.scala +++ b/src/test/scala/MockHelpers.scala @@ -21,7 +21,8 @@ trait MockHelpers { * @param composeMock Mock instance of the Plugin */ def mockDockerCommandCalls(composeMock: DockerComposePluginLocal): Unit = { - doNothing().when(composeMock).dockerComposeRemoveContainers(anyString, anyString) - doNothing().when(composeMock).dockerComposeStopInstance(anyString, anyString) + doNothing().when(composeMock).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) + doNothing().when(composeMock).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) + doNothing().when(composeMock).dockerComposeUp(anyString, anyString, any[Vector[(String, String)]]) } } diff --git a/src/test/scala/VariableSubstitutionSpec.scala b/src/test/scala/VariableSubstitutionSpec.scala new file mode 100644 index 0000000..2b23e27 --- /dev/null +++ b/src/test/scala/VariableSubstitutionSpec.scala @@ -0,0 +1,37 @@ +import com.tapad.docker.DockerComposeKeys._ +import com.tapad.docker.DockerComposePlugin._ +import com.tapad.docker.{ Version, RunningInstanceInfo, DockerComposePluginLocal } +import org.mockito.Matchers._ +import org.mockito.Mockito._ +import org.scalatest.{ BeforeAndAfter, FunSuite, OneInstancePerTest } +import sbt.State + +class VariableSubstitutionSpec extends FunSuite with OneInstancePerTest with MockHelpers { + test("Validate that provided variables are passed to docker-compose up command") { + val composeMock = spy(new DockerComposePluginLocal) + val serviceName = "matchingservice" + + val variables = Map("foo" -> "bar") + val actualVariables = variables.toVector + + doReturn(variables).when(composeMock).getSetting(variablesForSubstitution)(null) + + doReturn(null).when(composeMock).getSetting(composeFile)(null) + doReturn(null).when(composeMock).readComposeFile(null) + doReturn(null).when(composeMock).processCustomTags(null, null, null) + doReturn(null).when(composeMock).saveComposeFile(null) + doNothing().when(composeMock).pullDockerImages(null, null) + doReturn(null).when(composeMock).generateInstanceName(null) + doReturn(null).when(composeMock).getRunningInstanceInfo(null, null, null, null, actualVariables) + doNothing().when(composeMock).printMappedPortInformation(null, null, dockerComposeVersion) + doReturn(null).when(composeMock).saveInstanceToSbtSession(null, null) + + mockDockerCommandCalls(composeMock) + mockSystemSettings(composeMock, serviceName, None) + + composeMock.startDockerCompose(null, null) + + verify(composeMock, times(1)).getRunningInstanceInfo(null, null, null, null, actualVariables) + verify(composeMock, times(1)).dockerComposeUp(null, null, actualVariables) + } +} From c28a9e2f587fee635b8ef86090dffffbd43bb1f8 Mon Sep 17 00:00:00 2001 From: seglo Date: Fri, 10 Jun 2016 22:39:57 -0400 Subject: [PATCH 2/4] Update README.md with info about variablesForSubstitution setting. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1fb92fe..8646889 100644 --- a/README.md +++ b/README.md @@ -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 passed as environment variables when calling docker-compose. There are several sample projects showing how to configure sbt-docker-compose that can be found in the [**examples**] (examples) folder. From e64279d6e2604ef20d44d8c970d30e761412cfbc Mon Sep 17 00:00:00 2001 From: seglo Date: Tue, 14 Jun 2016 19:38:01 +0200 Subject: [PATCH 3/4] Plugin performs docker-compose variable substitution --- README.md | 2 +- .../scala/com/tapad/docker/ComposeFile.scala | 24 ++++++++---- .../com/tapad/docker/DockerCommands.scala | 15 +++----- .../tapad/docker/DockerComposePlugin.scala | 13 +++---- src/test/resources/variable_substitution.yml | 4 ++ .../scala/ComposeFileProcessingSpec.scala | 11 ++++++ src/test/scala/InstanceStoppingSpec.scala | 23 ++++++------ src/test/scala/MockHelpers.scala | 5 +-- src/test/scala/VariableSubstitutionSpec.scala | 37 ------------------- 9 files changed, 58 insertions(+), 76 deletions(-) create mode 100644 src/test/resources/variable_substitution.yml delete mode 100644 src/test/scala/VariableSubstitutionSpec.scala diff --git a/README.md b/README.md index 8646889..f402626 100644 --- a/README.md +++ b/README.md @@ -67,7 +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 passed as environment variables when calling docker-compose. + 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. diff --git a/src/main/scala/com/tapad/docker/ComposeFile.scala b/src/main/scala/com/tapad/docker/ComposeFile.scala index 1787780..1c506d7 100644 --- a/src/main/scala/com/tapad/docker/ComposeFile.scala +++ b/src/main/scala/com/tapad/docker/ComposeFile.scala @@ -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 diff --git a/src/main/scala/com/tapad/docker/DockerCommands.scala b/src/main/scala/com/tapad/docker/DockerCommands.scala index 9a6c3ca..6397e44 100644 --- a/src/main/scala/com/tapad/docker/DockerCommands.scala +++ b/src/main/scala/com/tapad/docker/DockerCommands.scala @@ -2,21 +2,18 @@ package com.tapad.docker import sbt._ import com.tapad.docker.DockerComposeKeys._ -import sys.process.Process trait DockerCommands { - def dockerComposeUp(instanceName: String, composePath: String, variables: Vector[(String, String)]): Unit = { - Process(s"docker-compose -p $instanceName -f $composePath up -d", None, variables: _*).run(false) + def dockerComposeUp(instanceName: String, composePath: String): Unit = { + s"docker-compose -p $instanceName -f $composePath up -d".! } - def dockerComposeStopInstance(instanceName: String, composePath: String, - variables: Vector[(String, String)] = Vector.empty): Unit = { - Process(s"docker-compose -p $instanceName -f $composePath stop", None, variables: _*).run(false) + def dockerComposeStopInstance(instanceName: String, composePath: String): Unit = { + s"docker-compose -p $instanceName -f $composePath stop".! } - def dockerComposeRemoveContainers(instanceName: String, composePath: String, - variables: Vector[(String, String)] = Vector.empty): Unit = { - Process(s"docker-compose -p $instanceName -f $composePath rm -v -f", None, variables: _*).run(false) + def dockerComposeRemoveContainers(instanceName: String, composePath: String): Unit = { + s"docker-compose -p $instanceName -f $composePath rm -v -f".! } def getDockerComposeVersion: Version = { diff --git a/src/main/scala/com/tapad/docker/DockerComposePlugin.scala b/src/main/scala/com/tapad/docker/DockerComposePlugin.scala index 17355d4..ab69874 100644 --- a/src/main/scala/com/tapad/docker/DockerComposePlugin.scala +++ b/src/main/scala/com/tapad/docker/DockerComposePlugin.scala @@ -42,8 +42,7 @@ 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 passed as environment variables to docker-compose for variable - * substitution + * @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, @@ -157,7 +156,7 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo 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") @@ -169,7 +168,7 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo val instanceName = generateInstanceName(state) val newState = Try { - dockerComposeUp(instanceName, updatedComposePath, variables) + dockerComposeUp(instanceName, updatedComposePath) val newInstance = getRunningInstanceInfo(state, instanceName, updatedComposePath, servicesInfo, variables) printMappedPortInformation(state, newInstance, dockerComposeVersion) @@ -248,17 +247,17 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo def stopLocalDockerInstance(implicit state: State, instanceName: String, composePath: String, variables: Vector[(String, String)]): Unit = { - dockerComposeStopInstance(instanceName, composePath, variables) + dockerComposeStopInstance(instanceName, composePath) if (getSetting(composeRemoveContainersOnShutdown)) { - dockerComposeRemoveContainers(instanceName, composePath, variables) + dockerComposeRemoveContainers(instanceName, composePath) } if (getSetting(composeRemoveNetworkOnShutdown)) { // 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) if (getComposeVersion(composeYaml) >= 2) { val dockerMachine = getSetting(dockerMachineName) dockerRemoveNetwork(instanceName, dockerMachine) diff --git a/src/test/resources/variable_substitution.yml b/src/test/resources/variable_substitution.yml new file mode 100644 index 0000000..e4312bf --- /dev/null +++ b/src/test/resources/variable_substitution.yml @@ -0,0 +1,4 @@ +testservice: + image: testservice:0.0.1 + ports: + - "${SOURCE_PORT}:5005" \ No newline at end of file diff --git a/src/test/scala/ComposeFileProcessingSpec.scala b/src/test/scala/ComposeFileProcessingSpec.scala index b0c3082..1eb61e6 100644 --- a/src/test/scala/ComposeFileProcessingSpec.scala +++ b/src/test/scala/ComposeFileProcessingSpec.scala @@ -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) diff --git a/src/test/scala/InstanceStoppingSpec.scala b/src/test/scala/InstanceStoppingSpec.scala index b6e7e95..b16f3cb 100644 --- a/src/test/scala/InstanceStoppingSpec.scala +++ b/src/test/scala/InstanceStoppingSpec.scala @@ -8,9 +8,8 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance val instanceId = "instanceId" val composePath = "path" val serviceName = "service" - val variables = Vector[(String, String)](("foo", "bar")) val composeMock = spy(new DockerComposePluginLocal) - val instance = RunningInstanceInfo(instanceId, serviceName, composePath, List.empty, variables) + val instance = RunningInstanceInfo(instanceId, serviceName, composePath, List.empty) mockDockerCommandCalls(composeMock) mockSystemSettings(composeMock, serviceName, Some(List(instance))) @@ -18,8 +17,8 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance composeMock.stopRunningInstances(null, Seq.empty) //Validate that the instance was stopped and cleaned up - verify(composeMock, times(1)).dockerComposeStopInstance(instanceId, composePath, variables) - verify(composeMock, times(1)).dockerComposeRemoveContainers(instanceId, composePath, variables) + verify(composeMock, times(1)).dockerComposeStopInstance(instanceId, composePath) + verify(composeMock, times(1)).dockerComposeRemoveContainers(instanceId, composePath) } test("Validate the proper stopping of a multiple instances when no specific instances are passed in as arguments") { @@ -36,8 +35,8 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance composeMock.stopRunningInstances(null, Seq.empty) //Validate that the instance was stopped and cleaned up - verify(composeMock, times(2)).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) - verify(composeMock, times(2)).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) + verify(composeMock, times(2)).dockerComposeStopInstance(anyString, anyString) + verify(composeMock, times(2)).dockerComposeRemoveContainers(anyString, anyString) } test("Validate the proper stopping of a single instance when multiple instances are running") { @@ -56,8 +55,8 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance //Validate that only once instance was Stopped and Removed verify(composeMock, times(1)).setAttribute(any, any)(any[sbt.State]) - verify(composeMock, times(1)).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) - verify(composeMock, times(1)).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) + verify(composeMock, times(1)).dockerComposeStopInstance(anyString, anyString) + verify(composeMock, times(1)).dockerComposeRemoveContainers(anyString, anyString) } test("Validate that only instances from the current SBT project are stopped when no arguments are supplied to DockerComposeStop") { @@ -74,8 +73,8 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance //Validate that only once instance was Stopped and Removed verify(composeMock, times(1)).setAttribute(any, any)(any[sbt.State]) - verify(composeMock, times(2)).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) - verify(composeMock, times(2)).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) + verify(composeMock, times(2)).dockerComposeStopInstance(anyString, anyString) + verify(composeMock, times(2)).dockerComposeRemoveContainers(anyString, anyString) } test("Validate that instances from any SBT project can be stopped when explicitly passed to DockerComposeStop") { @@ -92,7 +91,7 @@ class InstanceStoppingSpec extends FunSuite with BeforeAndAfter with OneInstance //Validate that only once instance was Stopped and Removed verify(composeMock, times(1)).setAttribute(any, any)(any[sbt.State]) - verify(composeMock, times(1)).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) - verify(composeMock, times(1)).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) + verify(composeMock, times(1)).dockerComposeStopInstance(anyString, anyString) + verify(composeMock, times(1)).dockerComposeRemoveContainers(anyString, anyString) } } diff --git a/src/test/scala/MockHelpers.scala b/src/test/scala/MockHelpers.scala index a7e8f45..16f44b4 100644 --- a/src/test/scala/MockHelpers.scala +++ b/src/test/scala/MockHelpers.scala @@ -21,8 +21,7 @@ trait MockHelpers { * @param composeMock Mock instance of the Plugin */ def mockDockerCommandCalls(composeMock: DockerComposePluginLocal): Unit = { - doNothing().when(composeMock).dockerComposeRemoveContainers(anyString, anyString, any[Vector[(String, String)]]) - doNothing().when(composeMock).dockerComposeStopInstance(anyString, anyString, any[Vector[(String, String)]]) - doNothing().when(composeMock).dockerComposeUp(anyString, anyString, any[Vector[(String, String)]]) + doNothing().when(composeMock).dockerComposeRemoveContainers(anyString, anyString) + doNothing().when(composeMock).dockerComposeStopInstance(anyString, anyString) } } diff --git a/src/test/scala/VariableSubstitutionSpec.scala b/src/test/scala/VariableSubstitutionSpec.scala deleted file mode 100644 index 2b23e27..0000000 --- a/src/test/scala/VariableSubstitutionSpec.scala +++ /dev/null @@ -1,37 +0,0 @@ -import com.tapad.docker.DockerComposeKeys._ -import com.tapad.docker.DockerComposePlugin._ -import com.tapad.docker.{ Version, RunningInstanceInfo, DockerComposePluginLocal } -import org.mockito.Matchers._ -import org.mockito.Mockito._ -import org.scalatest.{ BeforeAndAfter, FunSuite, OneInstancePerTest } -import sbt.State - -class VariableSubstitutionSpec extends FunSuite with OneInstancePerTest with MockHelpers { - test("Validate that provided variables are passed to docker-compose up command") { - val composeMock = spy(new DockerComposePluginLocal) - val serviceName = "matchingservice" - - val variables = Map("foo" -> "bar") - val actualVariables = variables.toVector - - doReturn(variables).when(composeMock).getSetting(variablesForSubstitution)(null) - - doReturn(null).when(composeMock).getSetting(composeFile)(null) - doReturn(null).when(composeMock).readComposeFile(null) - doReturn(null).when(composeMock).processCustomTags(null, null, null) - doReturn(null).when(composeMock).saveComposeFile(null) - doNothing().when(composeMock).pullDockerImages(null, null) - doReturn(null).when(composeMock).generateInstanceName(null) - doReturn(null).when(composeMock).getRunningInstanceInfo(null, null, null, null, actualVariables) - doNothing().when(composeMock).printMappedPortInformation(null, null, dockerComposeVersion) - doReturn(null).when(composeMock).saveInstanceToSbtSession(null, null) - - mockDockerCommandCalls(composeMock) - mockSystemSettings(composeMock, serviceName, None) - - composeMock.startDockerCompose(null, null) - - verify(composeMock, times(1)).getRunningInstanceInfo(null, null, null, null, actualVariables) - verify(composeMock, times(1)).dockerComposeUp(null, null, actualVariables) - } -} From 811fbc0fa21172eb5a55dc98c3342e19f08708f7 Mon Sep 17 00:00:00 2001 From: seglo Date: Tue, 14 Jun 2016 22:24:35 +0200 Subject: [PATCH 4/4] Add example docs. Reduce passing variables around. --- README.md | 18 ++++++++++++++++++ .../tapad/docker/DockerComposePlugin.scala | 19 ++++++++----------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f402626..0bcc40f 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,24 @@ launch a running instance that consists of both images: Note how the docker-compose.yml file for the root project tags each image with "\". This allows dockerComposeUp to know that these images should not be updated from the Docker Registry. +5) [**basic-variable-substitution**] (examples/basic-variable-substitution): This project demonstrates how you can re-use your +existing docker-compose.yml with [variable substitution](https://docs.docker.com/compose/compose-file/#variable-substitution) +using sbt-docker-compose. Instead of passing your variables as environment variables you can define them in your build.sbt +programmatically. + +build.sbt: + + variablesForSubstitution := Map("SOURCE_PORT" -> "5555") + +docker-compose.yml: + + 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" + Currently Unsupported Docker Compose Fields ------------------------------------------- 1) "build:" - All docker compose services need to specify an "image:" field. diff --git a/src/main/scala/com/tapad/docker/DockerComposePlugin.scala b/src/main/scala/com/tapad/docker/DockerComposePlugin.scala index ab69874..b3abc02 100644 --- a/src/main/scala/com/tapad/docker/DockerComposePlugin.scala +++ b/src/main/scala/com/tapad/docker/DockerComposePlugin.scala @@ -42,12 +42,10 @@ 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], variables: Vector[(String, String)] = Vector.empty, - instanceData: Option[Any] = None) + servicesInfo: Iterable[ServiceInfo], instanceData: Option[Any] = None) object DockerComposePlugin extends DockerComposePluginLocal { override def projectSettings = DockerComposeSettings.baseDockerComposeSettings @@ -169,12 +167,12 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo val newState = Try { dockerComposeUp(instanceName, updatedComposePath) - val newInstance = getRunningInstanceInfo(state, instanceName, updatedComposePath, servicesInfo, variables) + val newInstance = getRunningInstanceInfo(state, instanceName, updatedComposePath, servicesInfo) printMappedPortInformation(state, newInstance, dockerComposeVersion) saveInstanceToSbtSession(state, newInstance) } getOrElse { - stopLocalDockerInstance(state, instanceName, updatedComposePath, variables) + stopLocalDockerInstance(state, instanceName, updatedComposePath) throw new IllegalStateException(s"Error starting Docker Compose instance. Shutting down containers...") } @@ -184,14 +182,14 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo } def getRunningInstanceInfo(implicit state: State, instanceName: String, composePath: String, - servicesInfo: Iterable[ServiceInfo], variables: Vector[(String, String)]): RunningInstanceInfo = { + servicesInfo: Iterable[ServiceInfo]): 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, variables) + RunningInstanceInfo(instanceName, composeService, composePath, serviceInfo) } def pullDockerImages(args: Seq[String], services: Iterable[ServiceInfo]): Unit = { @@ -224,7 +222,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, instance.variables) + stopLocalDockerInstance(state, instance.instanceName, instance.composeFilePath) } if (removeList.isEmpty) @@ -245,8 +243,7 @@ class DockerComposePluginLocal extends AutoPlugin with ComposeFile with DockerCo updatedState } - def stopLocalDockerInstance(implicit state: State, instanceName: String, composePath: String, - variables: Vector[(String, String)]): Unit = { + def stopLocalDockerInstance(implicit state: State, instanceName: String, composePath: String): Unit = { dockerComposeStopInstance(instanceName, composePath) if (getSetting(composeRemoveContainersOnShutdown)) { @@ -257,7 +254,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, variables) + val composeYaml = readComposeFile(composePath) if (getComposeVersion(composeYaml) >= 2) { val dockerMachine = getSetting(dockerMachineName) dockerRemoveNetwork(instanceName, dockerMachine)