diff --git a/app/ansible/PlaybookGenerator.scala b/app/ansible/PlaybookGenerator.scala index 3849bd28..2d7a6d1b 100644 --- a/app/ansible/PlaybookGenerator.scala +++ b/app/ansible/PlaybookGenerator.scala @@ -20,7 +20,7 @@ object PlaybookGenerator { if (role.variables.isEmpty) role.roleId.value else - s"{ role: ${role.roleId.value}, ${role.variables.map { case (k, v) => s"$k: '$v'" }.mkString(", ")} }" + s"{ role: ${role.roleId.value}, ${role.variables.map { case (k, v) => s"$k: ${v.quoted}" }.mkString(", ")} }" } } diff --git a/app/data/Recipes.scala b/app/data/Recipes.scala index 912ac13d..21448c7a 100644 --- a/app/data/Recipes.scala +++ b/app/data/Recipes.scala @@ -3,7 +3,6 @@ package data import com.amazonaws.services.dynamodbv2.model._ import models._ import org.joda.time.DateTime - import com.gu.scanamo.syntax._ import scala.collection.JavaConverters._ diff --git a/app/models/CustomisedRole.scala b/app/models/CustomisedRole.scala index c11341dd..0b929e59 100644 --- a/app/models/CustomisedRole.scala +++ b/app/models/CustomisedRole.scala @@ -1,8 +1,13 @@ package models +import cats.data.Xor +import com.gu.scanamo.DynamoFormat +import com.gu.scanamo.error.TypeCoercionError +import fastparse.WhitespaceApi + case class CustomisedRole( roleId: RoleId, - variables: Map[String, String]) { + variables: Map[String, ParamValue]) { def variablesToString = variables.map { case (k, v) => s"$k: $v" }.mkString("{ ", ", ", " }") @@ -14,12 +19,48 @@ case class CustomisedRole( } +sealed trait ParamValue { def quoted: String } +case class SingleParamValue(param: String) extends ParamValue { + override def toString: String = param + val quoted = s"'$param'" +} +case class ListParamValue(params: List[SingleParamValue]) extends ParamValue { + override def toString: String = s"[${params.mkString(", ")}]" + val quoted = s"[${params.map(_.quoted).mkString(", ")}]" +} +object ListParamValue { + def of(params: String*) = ListParamValue(params.map(SingleParamValue).toList) +} +object ParamValue { + implicit val format = DynamoFormat.xmap[ParamValue, String]( + CustomisedRole.paramValue.parse(_).fold( + (_, _, _) => Xor.left(TypeCoercionError(new RuntimeException("Unable to read ParamValue"))), + (pv, _) => Xor.right(pv)) + )(_.toString) +} + object CustomisedRole { + val White = WhitespaceApi.Wrapper { + import fastparse.all._ + NoTrace(" ".rep) + } + import fastparse.noApi._ + import White._ + + val key: Parser[String] = P(CharsWhile(_ != ':').!) + val singleValue: Parser[SingleParamValue] = P(CharPred(c => c.isLetterOrDigit || c == '-' || c == '_').rep.!).map(SingleParamValue(_)) + val multiValues: Parser[ListParamValue] = P("[" ~ singleValue.rep(sep = ",") ~ "]").map( + params => ListParamValue(params.toList)) + val paramValue: Parser[ParamValue] = multiValues | singleValue + val pair: Parser[(String, ParamValue)] = P(key ~ ":" ~ paramValue) - private val KeyValuePair = """(.+): ?(.+)""".r + val parameters = P(Start ~ pair.rep(sep = ",") ~ End) - def formInputTextToVariables(input: String): Map[String, String] = { - input.split(", *").collect { case KeyValuePair(k, v) => (k.trim, v.trim) }.toMap + def formInputTextToVariables(input: String): Map[String, ParamValue] = { + parameters.parse(input) match { + case Parsed.Success(value, _) => value.toMap + case f: Parsed.Failure => Map.empty + } } } diff --git a/build.sbt b/build.sbt index 515d0084..f70db69f 100644 --- a/build.sbt +++ b/build.sbt @@ -26,6 +26,7 @@ libraryDependencies ++= Seq( "com.gu" %% "play-googleauth" % "0.4.0", "com.adrianhurt" %% "play-bootstrap3" % "0.4.5-P24", "org.quartz-scheduler" % "quartz" % "2.2.3", + "com.lihaoyi" %% "fastparse" % "0.4.1", "org.scalatest" %% "scalatest" % "2.2.6" % Test ) routesGenerator := InjectedRoutesGenerator diff --git a/roles/packages/README.md b/roles/packages/README.md new file mode 100644 index 00000000..7565f916 --- /dev/null +++ b/roles/packages/README.md @@ -0,0 +1,7 @@ +# packages + +Install package(s) with `apt-get` or `yum`. The `packages` parameter specifies which package(s) are installed, e.g. + +``` +packages: [package-1, package-2] +``` \ No newline at end of file diff --git a/roles/packages/meta/main.yml b/roles/packages/meta/main.yml new file mode 100644 index 00000000..ed417917 --- /dev/null +++ b/roles/packages/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: apt + when: ansible_os_family == "Debian" diff --git a/roles/packages/tasks/main.yml b/roles/packages/tasks/main.yml new file mode 100644 index 00000000..ba727d53 --- /dev/null +++ b/roles/packages/tasks/main.yml @@ -0,0 +1,10 @@ +--- +- name: Install a package with apt-get + apt: name={{ item }} state=present + with_items: "{{ packages }}" + when: ansible_os_family == "Debian" + +- name: Install a package with yum + yum: name={{ item }} state=present + with_items: "{{ packages }}" + when: ansible_os_family == "RedHat" \ No newline at end of file diff --git a/test/ansible/PlaybookGeneratorSpec.scala b/test/ansible/PlaybookGeneratorSpec.scala index d5e50814..f28ae593 100644 --- a/test/ansible/PlaybookGeneratorSpec.scala +++ b/test/ansible/PlaybookGeneratorSpec.scala @@ -15,7 +15,7 @@ class PlaybookGeneratorSpec extends FlatSpec with Matchers { description = "", amiId = AmiId(""), builtinRoles = List( - CustomisedRole(RoleId("builtinRole1"), Map("foo" -> "bar")), + CustomisedRole(RoleId("builtinRole1"), Map("foo" -> SingleParamValue("bar"))), CustomisedRole(RoleId("builtinRole2"), Map.empty) ), createdBy = "Testy McTest", @@ -24,7 +24,7 @@ class PlaybookGeneratorSpec extends FlatSpec with Matchers { modifiedAt = DateTime.now() ), roles = List( - CustomisedRole(RoleId("recipeRole1"), Map("wow" -> "yeah")), + CustomisedRole(RoleId("recipeRole1"), Map("wow" -> ListParamValue.of("yeah", "bonza"))), CustomisedRole(RoleId("recipeRole2"), Map.empty) ), createdBy = "Testy McTest", @@ -42,7 +42,7 @@ class PlaybookGeneratorSpec extends FlatSpec with Matchers { | roles: | - { role: builtinRole1, foo: 'bar' } | - builtinRole2 - | - { role: recipeRole1, wow: 'yeah' } + | - { role: recipeRole1, wow: ['yeah', 'bonza'] } | - recipeRole2 |""".stripMargin ) diff --git a/test/models/CustomisedRoleTest.scala b/test/models/CustomisedRoleTest.scala new file mode 100644 index 00000000..a502e233 --- /dev/null +++ b/test/models/CustomisedRoleTest.scala @@ -0,0 +1,21 @@ +package models + +import org.scalatest.{ FunSuite, ShouldMatchers } + +class CustomisedRoleTest extends FunSuite with ShouldMatchers { + + test("should parse a map of variables") { + CustomisedRole.formInputTextToVariables("ssh_keys_bucket: bucket, ssh_keys_prefix: Team") + .shouldBe(Map("ssh_keys_bucket" -> SingleParamValue("bucket"), "ssh_keys_prefix" -> SingleParamValue("Team"))) + } + + test("should parse variable lists") { + CustomisedRole.formInputTextToVariables("packages: [python-pip, emacs24]") + .shouldBe(Map("packages" -> ListParamValue.of("python-pip", "emacs24"))) + } + + test("should round trip param values") { + def roundTrip(pv: ParamValue) = ParamValue.format.read(ParamValue.format.write(pv)).toOption.get + roundTrip(SingleParamValue("X")) shouldBe (SingleParamValue("X")) + } +}