Add BazelConfiguration groovy class
This is just a convenience class to read configuration from
hierarchical json descriptor and return a list of configuration
to execute. Each configuration contains a descriptor (key=value pair,
e.g. the node label to run on) and a set of configuration parameters.
This change also add a BUILD file and some test to the groovy library
so we need to change the way we ship the library inside the docker
container.
Change-Id: I5d6a2090b4360a0497b96e466df65ee52d19bbdb
diff --git a/WORKSPACE b/WORKSPACE
index a868c1f..8c704fd 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -103,3 +103,29 @@
remote = "https://github.com/google/python-gflags",
tag = "python-gflags-2.0",
)
+
+# Testing Jenkins pipeline library
+# TODO(dmarting): the groovy support is really rudimentary we should fix it:
+# - Need for adding more dependency
+# - Groovy test absolutely want you to declare a specific structure
+# - The release is not working with latest bazel
+# - Repository overrely on bind() and does not respect naming conventions
+http_archive(
+ name = "io_bazel_rules_groovy",
+ url = "https://github.com/bazelbuild/rules_groovy/archive/1256063915da0e46c229ce93489175f0d084f0cb.zip",
+ sha256 = "0e7a425e3ebcc649c4fac9876ebbfdb3948bf97c47954ffda3e15b32a5bf4b6e",
+ strip_prefix = "rules_groovy-1256063915da0e46c229ce93489175f0d084f0cb",
+)
+load("@io_bazel_rules_groovy//groovy:groovy.bzl", "groovy_repositories")
+groovy_repositories()
+
+# For groovy tests
+maven_jar(
+ name = "org_codehaus_groovy_all",
+ artifact = "org.codehaus.groovy:groovy-all:jar:2.4.4",
+)
+
+maven_jar(
+ name = "org_hamcrest",
+ artifact = "org.hamcrest:hamcrest-all:jar:1.3",
+)
diff --git a/jenkins/BUILD b/jenkins/BUILD
index 727fea9..4124a2e 100644
--- a/jenkins/BUILD
+++ b/jenkins/BUILD
@@ -301,6 +301,7 @@
"SECURITY_CONFIG": SECURITY_CONFIG,
"PUBLIC_JENKINS_URL": "http://ci.bazel.io/",
} + JOBS_SUBSTITUTIONS,
+ tars = ["//jenkins/lib"],
visibility = ["//visibility:public"],
)
@@ -325,6 +326,7 @@
"SECURITY_CONFIG": SECURITY_CONFIG,
"PUBLIC_JENKINS_URL": "http://ci-staging.bazel.io/",
} + STAGING_JOBS_SUBSTITUTIONS,
+ tars = ["//jenkins/lib"],
visibility = ["//gcr:__pkg__"],
)
@@ -345,6 +347,7 @@
"SECURITY_CONFIG": "<useSecurity>false</useSecurity>",
"PUBLIC_JENKINS_URL": "##ENV:JENKINS_SERVER##",
} + JOBS_SUBSTITUTIONS,
+ tars = ["//jenkins/lib"],
)
filegroup(
diff --git a/jenkins/jenkins.bzl b/jenkins/jenkins.bzl
index 30ec2c8..6410d31 100644
--- a/jenkins/jenkins.bzl
+++ b/jenkins/jenkins.bzl
@@ -400,7 +400,7 @@
)
def jenkins_build(name, plugins = None, base = "//jenkins/base", configs = [],
- jobs = [], substitutions = {}, visibility = None):
+ jobs = [], substitutions = {}, visibility = None, tars = []):
"""Build the docker image for the Jenkins instance."""
substitutions = substitutions + MAILS_SUBSTITUTIONS
# Expands config files in a tar ball
@@ -422,22 +422,13 @@
directory = "/usr/share/jenkins/ref",
)
- # Create a tar of the library files
- pkg_tar(
- name = "%s-lib" % name,
- files = native.glob(["lib/**"]),
- strip_prefix = "lib",
- package_dir = "/opt/lib",
- )
-
### FINAL IMAGE ###
docker_build(
name = name,
tars = [
":%s-jobs" % name,
":%s-configs" % name,
- ":%s-lib" % name,
- ],
+ ] + tars,
# Workaround no way to specify owner in pkg_tar
# TODO(dmarting): use https://cr.bazel.build/10255 when it hits a release.
user = "root",
diff --git a/jenkins/lib/BUILD b/jenkins/lib/BUILD
new file mode 100644
index 0000000..b15b2f1
--- /dev/null
+++ b/jenkins/lib/BUILD
@@ -0,0 +1,33 @@
+package(default_visibility = ["//jenkins:__subpackages__"])
+
+load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar")
+load("@io_bazel_rules_groovy//groovy:groovy.bzl", "groovy_library")
+
+pkg_tar(
+ name = "lib",
+ files = glob([
+ "src/build/**",
+ "vars/**",
+ ]),
+ package_dir = "/opt/lib",
+ strip_prefix = ".",
+)
+
+groovy_library(
+ name = "BazelConfigurationTests",
+ srcs = [
+ "src/build/bazel/ci/BazelConfiguration.groovy",
+ "tests/build/bazel/ci/BazelConfigurationTests.groovy",
+ ],
+)
+
+java_test(
+ name = "BazelConfigurationTests-test",
+ size = "small",
+ runtime_deps = [
+ "@org_codehaus_groovy_all//jar",
+ "@org_hamcrest//jar",
+ ":BazelConfigurationTests",
+ ],
+ test_class = "build.bazel.ci.BazelConfigurationTests",
+)
diff --git a/jenkins/lib/src/build/bazel/ci/BazelConfiguration.groovy b/jenkins/lib/src/build/bazel/ci/BazelConfiguration.groovy
new file mode 100644
index 0000000..148dadb
--- /dev/null
+++ b/jenkins/lib/src/build/bazel/ci/BazelConfiguration.groovy
@@ -0,0 +1,187 @@
+// Copyright (C) 2017 The Bazel Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build.bazel.ci
+
+import groovy.json.JsonSlurper
+import groovy.json.JsonParserType
+
+/**
+ * A class that stores configuration for a Bazel job.
+ *
+ * A configuration is composed of a descriptor and a list of parameters.
+ * A descriptor is a key-value map describing the configuration itself and
+ * parameters are parameters for the job common to all configurations but for
+ * which the value is configuration specific. E.g. test options or targets to
+ * build.
+ *
+ * A configuration can contains several other configurations. In which case the
+ * child configurations get factored with the parent configuration to create N
+ * configurations that inherit the parameters and descriptor of the parent
+ * configuration. If a child configuration specify a value already present in the
+ * parent configuration, the parent configuration value will be ignored and the child
+ * configuration value will be used.
+ *
+ * Example:
+ * BazelConfiguration(["descriptor": "yeah"],
+ * ["params1": false],
+ * [BazelConfiguration(["descriptor2": "a"], [], [params2: true]),
+ * BazelConfiguration(["descriptor2": "b"], [], [params1: true, params2: false])])
+ *
+ * would expand to the following configurations:
+ *
+ * BazelConfiguration(["descriptor": "yeah", "descriptor2": "a"], [params1: false, params2: true])])
+ * BazelConfiguration(["descriptor": "yeah", "descriptor2": "b"], [params1: true, params2: false])])
+ */
+class BazelConfiguration implements java.io.Serializable {
+ private Map<String, String> descriptor
+ private Map<String, Object> parameters
+ private List<BazelConfiguration> configurations
+
+ static public interface ConfigurationContainer {
+ def addConfiguration(BazelConfiguration)
+ }
+
+ private static List<BazelConfiguration> parseJson(Object json) {
+ List<BazelConfiguration> result = []
+ for (Object o in json) {
+ result.add(new BazelConfiguration(o))
+ }
+ return result
+ }
+
+ private static Object toSerializable(Object jsonObject) {
+ // JsonSlurper map and list are not serializable, which make them unsuitable for
+ // usage inside a Jenkins pipeline, convert them to HashMap and ArrayList
+ if (jsonObject instanceof Map) {
+ def result = [:]
+ for (e in jsonObject) {
+ result[e.key] = toSerializable(e.value)
+ }
+ return result
+ } else if (jsonObject instanceof List) {
+ def result = []
+ for (it in jsonObject) {
+ result.add(toSerializable(it))
+ }
+ return result
+ } else {
+ return jsonObject
+ }
+ }
+
+ /**
+ * Parse a list of configurations for a JSON string.
+ * A JSON configuration is an object whose key -> values are the entries
+ * of the configuration descriptor, except for "configurations" and "parameters"
+ * keys which correspond respectively to the child configurations and the parameters.
+ */
+ public static List<BazelConfiguration> parse(String jsonString) {
+ // There is a subtle bug in the parser so skip the first comment line
+ while (jsonString.trim().startsWith("//")) {
+ def pos = jsonString.indexOf("\n")
+ jsonString = pos < 0 ? "" : jsonString.substring(pos + 1)
+ }
+ def jsonObject = new JsonSlurper().setType(JsonParserType.LAX).parseText(jsonString);
+ return parseJson(toSerializable(jsonObject))
+ }
+
+ /** Parse a list of configurations from a JSON file. */
+ public static List<BazelConfiguration> parse(File jsonFile) {
+ return parse(jsonFile.text)
+ }
+
+ /**
+ * Flatten a list of configurations into a map of descriptor -> parameters.
+ *
+ * Restrictions can be applied to configuration to select only configuration compatible with
+ * the environment we run on (e.g. testing environment inside docker has only linux slave available).
+ * The restrictions are specified by a map of descriptor key-values. A configuration will be selected
+ * only if, for any key k in the descriptor, either the k is not a key of configurationRestrictions
+ * or the value of the descriptor is in configurationRestrictions[k].
+ *
+ * Examples:
+ * - configurationRestrictions = ["a": ["a", "b"], "b": []] would match descriptor
+ * ["a": "a"], ["a": "b"], ["c": "whatever"] but not ["a": "c"] nor ["b": "whatever"]
+ * - configurationRestrictions = ["node": ["linux-x86_64"]] would match only the descriptors that
+ * point to an execution on a linux node.
+ */
+ public static Map<Map<String, String>, Map<String, Object>> flattenConfigurations(
+ List<BazelConfiguration> configurations,
+ Map<String, List<String>> configurationRestrictions = [:]) {
+ def result = [:]
+ for (conf in configurations) {
+ result += conf.flatten(configurationRestrictions)
+ }
+ return result
+ }
+
+ private BazelConfiguration(Object json) {
+ this.parameters = ("parameters" in json) ? json["parameters"] : [:]
+ this.configurations = ("configurations" in json) ?
+ json["configurations"].collect { it -> new BazelConfiguration(it) } : []
+ this.descriptor = json.findAll { k, v -> k != "configurations" && k != "parameters" }
+ }
+
+ public BazelConfiguration(Map<String, String> descriptor,
+ Map<String, Object> parameters,
+ List<BazelConfiguration> configurations = []) {
+ this.descriptor = descriptor
+ this.parameters = parameters
+ this.configurations = configurations
+ }
+
+ def getDescriptor() {
+ return descriptor
+ }
+
+ def getParameters() {
+ return parameters
+ }
+
+ def getConfigurations() {
+ return configurations
+ }
+
+ private Map<Map<String, String>, Map<String, Object>> flatten(Map<String, List<String>> configurationRestrictions = [:]) {
+ Map<Map<String, String>, Map<String, Object>> result = [:]
+ if (descriptor.any {
+ k, v -> (k in configurationRestrictions) && !(v in configurationRestrictions[k]) }) {
+ return result
+ }
+
+ if (configurations.isEmpty()) {
+ if (!descriptor.isEmpty()) {
+ result[descriptor] = parameters
+ }
+ return result
+ } else {
+ for (conf in configurations) {
+ def configs = conf.flatten(configurationRestrictions)
+ for (e in configs) {
+ def descr2 = [:]
+ descr2.putAll(descriptor)
+ descr2.putAll(e.key)
+ result[descr2] = [:]
+ result[descr2].putAll(parameters)
+ result[descr2].putAll(e.value)
+ }
+ }
+ if (result.isEmpty() && !descriptor.isEmpty()) {
+ result[descriptor] = parameters
+ }
+ return result
+ }
+ }
+}
diff --git a/jenkins/lib/tests/build/bazel/ci/BazelConfigurationTests.groovy b/jenkins/lib/tests/build/bazel/ci/BazelConfigurationTests.groovy
new file mode 100644
index 0000000..0a670a7
--- /dev/null
+++ b/jenkins/lib/tests/build/bazel/ci/BazelConfigurationTests.groovy
@@ -0,0 +1,211 @@
+// Copyright (C) 2017 The Bazel Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build.bazel.ci
+
+import org.junit.Test
+
+/** Tests for {@link BazelConguration} */
+class BazelConfigurationTests {
+ static final String JSON_TEST = '''
+
+// This is a test
+// Double comment to test the workaround the parser issue
+
+// more comment
+// And now the initial bazel tests
+[
+ {
+ // This is a configuration that have 3 subconfiguration: linux, ubuntu and darwin
+ // Each of those configuration have 4 bazel variations: HEAD, HEAD-jdk7, latest,
+ // and latest-jdk7
+ "configurations": [
+ {
+ "node": "linux-x86_64",
+ "configurations": [
+ // XXX(dmarting): Remove HEAD from here.
+ {"variation": "HEAD"},
+ {"variation": "HEAD-jdk7"},
+ {"variation": "latest"},
+ {"variation": "latest-jdk7"}
+ ]
+ },
+ {
+ "node": "ubuntu_16.04-x86_64",
+ "configurations": [
+ {"variation": "HEAD"},
+ {"variation": "HEAD-jdk7"},
+ {"variation": "latest"},
+ {"variation": "latest-jdk7"}
+ ]
+ },
+ {
+ "node": "darwin-x86_64",
+ "configurations": [
+ {"variation": "HEAD"},
+ {"variation": "HEAD-jdk7"},
+ {"variation": "latest"},
+ {"variation": "latest-jdk7"}
+ ]
+ }
+ ],
+ // And specify the parameters for these configurations.
+ "parameters": {
+ "configure": [
+ "source scripts/ci/build.sh",
+ "setup_android_repositories"
+ ],
+ "test_opts": ["-k", "--build_tests_only"],
+ "tests": [
+ "//scripts/...",
+ "//src/...",
+ "//third_party/ijar/..."
+ ],
+ "targets": []
+ }
+ }, {
+ "toolchain": "msvc",
+ "configurations": [{
+ // XXX(dmarting): MSVC is a misnommer, it should have been called win32
+ // (for win32 native binary).
+ // XXX(dmarting): really MSVC/Win32 should be a bazel variation, not part of
+ // the node.
+ "node": "windows-msvc-x86_64",
+ "configurations": [{"variation": "HEAD"},{"variation": "latest"}]
+ }, {
+ "node": "windows-x86_64",
+ "configurations": [{"variation": "HEAD"},{"variation": "latest"}]
+ }],
+ "parameters": {
+ "test_opts": ["-k", "--build_tests_only"],
+ "tests": [
+ "//src/test/java/...",
+ "//src/test/cpp/...",
+ "//src/test/naive:all_tests"
+ ],
+ "targets": ["//src:bazel"]
+ }
+ }, {
+ "toolchain": "msys",
+ "configurations": [{
+ "node": "windows-msvc-x86_64",
+ "configurations": [{"variation": "HEAD"},{"variation": "latest"}]
+ }, {
+ "node": "windows-x86_64",
+ "configurations": [{"variation": "HEAD"},{"variation": "latest"}]
+ }],
+ "parameters": {
+ "test_opts": ["-k", "--build_tests_only"],
+ "tests": ["//src/tst/shell/bazel:bazel_windows_example_test"],
+ "targets": []
+ }
+ }
+]
+
+// Ending comment
+'''
+
+ private void assertConfigurationCorrect(confs) {
+ assert confs.size() == 3
+ assert confs[0].descriptor.size() == 0
+ assert confs[0].parameters.size() == 4
+ assert confs[0].parameters["configure"] == [
+ "source scripts/ci/build.sh",
+ "setup_android_repositories"
+ ]
+ assert confs[0].configurations.size() == 3
+ assert confs[0].configurations.every {
+ v -> v.configurations.size() == 4 && v.parameters.size() == 0 && v.descriptor.size() == 1 }
+ assert confs[1].descriptor.size() == 1
+ assert confs[1].parameters.size() == 3
+ assert confs[1].configurations.size() == 2
+ assert confs[1].configurations.every {
+ v -> v.configurations.size() == 2 && v.parameters.size() == 0 && v.descriptor.size() == 1 }
+ assert confs[2].descriptor.size() == 1
+ assert confs[2].parameters.size() == 3
+ assert confs[2].configurations.size() == 2
+ assert confs[2].configurations.every {
+ v -> v.configurations.size() == 2 && v.parameters.size() == 0 && v.descriptor.size() == 1 }
+ }
+
+ @Test
+ void testParseJsonString() {
+ assertConfigurationCorrect(BazelConfiguration.parse(JSON_TEST))
+ }
+
+ @Test
+ void testSerialization() {
+ // Just test that the object from parsing JSON is serializable
+ new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(BazelConfiguration.parse(JSON_TEST));
+ }
+
+ @Test
+ void testParseJsonFile() {
+ def tempDir = System.getenv("TEST_TMPDIR")
+ def tempDirFile = null
+ if (tempDir != null) {
+ tempDirFile = new File(tempDir)
+ }
+ def testFile = File.createTempFile('temp', '.json', tempDirFile)
+ try {
+ testFile.write(JSON_TEST)
+ assertConfigurationCorrect(BazelConfiguration.parse(testFile))
+ } finally {
+ testFile.delete()
+ }
+ }
+
+ @Test
+ void testFlatten() {
+ def result = BazelConfiguration.flattenConfigurations(BazelConfiguration.parse(JSON_TEST))
+ def allKeys = result.collect {
+ k, v -> k.collect { k1, v1 -> "${k1}=${v1}" }.toSorted().join(",") }.toSorted()
+ // Once flatten there are 20 configurations
+ assert allKeys.size() == 20
+ assert allKeys.join("\n") == '''node=darwin-x86_64,variation=HEAD
+node=darwin-x86_64,variation=HEAD-jdk7
+node=darwin-x86_64,variation=latest
+node=darwin-x86_64,variation=latest-jdk7
+node=linux-x86_64,variation=HEAD
+node=linux-x86_64,variation=HEAD-jdk7
+node=linux-x86_64,variation=latest
+node=linux-x86_64,variation=latest-jdk7
+node=ubuntu_16.04-x86_64,variation=HEAD
+node=ubuntu_16.04-x86_64,variation=HEAD-jdk7
+node=ubuntu_16.04-x86_64,variation=latest
+node=ubuntu_16.04-x86_64,variation=latest-jdk7
+node=windows-msvc-x86_64,toolchain=msvc,variation=HEAD
+node=windows-msvc-x86_64,toolchain=msvc,variation=latest
+node=windows-msvc-x86_64,toolchain=msys,variation=HEAD
+node=windows-msvc-x86_64,toolchain=msys,variation=latest
+node=windows-x86_64,toolchain=msvc,variation=HEAD
+node=windows-x86_64,toolchain=msvc,variation=latest
+node=windows-x86_64,toolchain=msys,variation=HEAD
+node=windows-x86_64,toolchain=msys,variation=latest'''
+ }
+
+ @Test
+ void testFlattenWithRestriction() {
+ def result = BazelConfiguration.flattenConfigurations(
+ BazelConfiguration.parse(JSON_TEST),
+ [node: ["linux-x86_64", "windows-x86_64"], variation: ["HEAD", "HEAD-jdk7"]])
+ def allKeys = result.collect {
+ k, v -> k.collect { k1, v1 -> "${k1}=${v1}" }.toSorted().join(",") }.toSorted()
+ assert allKeys.size() == 4
+ assert allKeys.join("\n") == '''node=linux-x86_64,variation=HEAD
+node=linux-x86_64,variation=HEAD-jdk7
+node=windows-x86_64,toolchain=msvc,variation=HEAD
+node=windows-x86_64,toolchain=msys,variation=HEAD'''
+ }
+}