From cc76e23787b72449b7338c95c8e7a46ef785af81 Mon Sep 17 00:00:00 2001 From: Philip Wills Date: Fri, 11 Nov 2016 14:36:45 +0000 Subject: [PATCH 1/2] Add role for generically installing an apt package --- roles/apt-install/README.md | 7 +++++++ roles/apt-install/meta/main.yml | 3 +++ roles/apt-install/tasks/main.yml | 3 +++ 3 files changed, 13 insertions(+) create mode 100644 roles/apt-install/README.md create mode 100644 roles/apt-install/meta/main.yml create mode 100644 roles/apt-install/tasks/main.yml diff --git a/roles/apt-install/README.md b/roles/apt-install/README.md new file mode 100644 index 00000000..d643fe8f --- /dev/null +++ b/roles/apt-install/README.md @@ -0,0 +1,7 @@ +# apt-install + +Install the named package with `apt-get`, takes a parameter to specify which package, e.g. + +``` +package: desired-package-name +``` \ No newline at end of file diff --git a/roles/apt-install/meta/main.yml b/roles/apt-install/meta/main.yml new file mode 100644 index 00000000..36af9022 --- /dev/null +++ b/roles/apt-install/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - apt diff --git a/roles/apt-install/tasks/main.yml b/roles/apt-install/tasks/main.yml new file mode 100644 index 00000000..d4b8e4c2 --- /dev/null +++ b/roles/apt-install/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: Install a package with apt-get + apt: name={{ package }} state=present From e1f6992918505f974cfa812ba4d0b4067fa2bdf4 Mon Sep 17 00:00:00 2001 From: Philip Wills Date: Thu, 17 Nov 2016 12:31:05 +0000 Subject: [PATCH 2/2] Add support for installing multiple packages Also made the task work with yum on RedHat derivatives --- app/ansible/PlaybookGenerator.scala | 2 +- app/data/Recipes.scala | 1 - app/models/CustomisedRole.scala | 49 ++++++++++++++++++++++-- build.sbt | 1 + roles/apt-install/README.md | 7 ---- roles/apt-install/meta/main.yml | 3 -- roles/apt-install/tasks/main.yml | 3 -- roles/packages/README.md | 7 ++++ roles/packages/meta/main.yml | 4 ++ roles/packages/tasks/main.yml | 10 +++++ test/ansible/PlaybookGeneratorSpec.scala | 6 +-- test/models/CustomisedRoleTest.scala | 21 ++++++++++ 12 files changed, 92 insertions(+), 22 deletions(-) delete mode 100644 roles/apt-install/README.md delete mode 100644 roles/apt-install/meta/main.yml delete mode 100644 roles/apt-install/tasks/main.yml create mode 100644 roles/packages/README.md create mode 100644 roles/packages/meta/main.yml create mode 100644 roles/packages/tasks/main.yml create mode 100644 test/models/CustomisedRoleTest.scala 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/apt-install/README.md b/roles/apt-install/README.md deleted file mode 100644 index d643fe8f..00000000 --- a/roles/apt-install/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# apt-install - -Install the named package with `apt-get`, takes a parameter to specify which package, e.g. - -``` -package: desired-package-name -``` \ No newline at end of file diff --git a/roles/apt-install/meta/main.yml b/roles/apt-install/meta/main.yml deleted file mode 100644 index 36af9022..00000000 --- a/roles/apt-install/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -dependencies: - - apt diff --git a/roles/apt-install/tasks/main.yml b/roles/apt-install/tasks/main.yml deleted file mode 100644 index d4b8e4c2..00000000 --- a/roles/apt-install/tasks/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -- name: Install a package with apt-get - apt: name={{ package }} state=present 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")) + } +}