Add structure test for base-container

So far the containers created in this project weren't tested
automatically.

This change adds structure tests for the base-container making a start
in the effort of implementing automated tests. The python package
'pytest' is used to run the test and is meant to provide a framework to
implement additional tests as well. Pipenv is used to ensure
a consistent test environment.

Change-Id: I04b1bdbc5b6360402b5a629c264c5c5c882f773f
diff --git a/.gitignore b/.gitignore
index 386ecda..873bf67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@
 /helm-charts/*/charts
 /helm-charts/*/requirements.lock
 
-__pycache__
\ No newline at end of file
+__pycache__
+.pytest_cache
diff --git a/.pylintrc b/.pylintrc
index 7ee00aa..d34fd37 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,5 +1,8 @@
 [MESSAGES CONTROL]
-disable=C0111
+disable=C0111, W0621
+
+[BASIC]
+no-docstring-rgx=(test_.*)|(__.*__)
 
 [FORMAT]
 indent-string='  '
diff --git a/LICENSE b/LICENSE
index 4137660..3b12405 100644
--- a/LICENSE
+++ b/LICENSE
@@ -209,11 +209,27 @@
 license terms. Your use of these subcomponents is subject to the separate
 license terms applicable to each subcomponent.
 
+Docker-Py \
+https://github.com/docker/docker-py \
+Copyright 2016 Docker, Inc \
+Apache 2 license (https://github.com/docker/docker-py/blob/master/LICENSE)
+
+GitPython \
+https://github.com/gitpython-developers/GitPython \
+Copyright (C) 2008, 2009 Michael Trier and contributors
+All rights reserved. \
+3-Clause BSD License (https://github.com/gitpython-developers/GitPython/blob/master/LICENSE)
+
 PyMySQL \
 https://github.com/PyMySQL/PyMySQL \
 Copyright (c) 2010, 2013 PyMySQL contributors \
 MIT License (https://github.com/PyMySQL/PyMySQL/blob/master/LICENSE)
 
+PyTest \
+https://github.com/pytest-dev/pytest \
+Copyright (c) 2004-2017 Holger Krekel and others \
+MIT License (https://github.com/pytest-dev/pytest/blob/master/LICENSE)
+
 SQLAlchemy \
 https://github.com/sqlalchemy/sqlalchemy/ \
 Copyright (c) 2005-2019 Michael Bayer and contributors.
@@ -244,3 +260,34 @@
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 ```
+
+## 3-Clause BSD License
+
+```
+Copyright <YEAR> <COPYRIGHT HOLDER>
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..1e65319
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,14 @@
+[[source]]
+name = "pypi"
+url = "https://pypi.org/simple"
+verify_ssl = true
+
+[dev-packages]
+
+[packages]
+docker = "~=3.5.1"
+gitpython = "~=2.1.11"
+pytest = "~=4.1"
+
+[requires]
+python_version = "3.7"
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000..9d2e652
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,151 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "18176217a070f600694b2169e71d07b6ea68f97805670f2cdd13c846a8329444"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.7"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "atomicwrites": {
+            "hashes": [
+                "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
+                "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
+            ],
+            "version": "==1.2.1"
+        },
+        "attrs": {
+            "hashes": [
+                "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
+                "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
+            ],
+            "version": "==18.2.0"
+        },
+        "certifi": {
+            "hashes": [
+                "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
+                "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
+            ],
+            "version": "==2018.11.29"
+        },
+        "chardet": {
+            "hashes": [
+                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
+                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+            ],
+            "version": "==3.0.4"
+        },
+        "docker": {
+            "hashes": [
+                "sha256:31421f16c01ffbd1ea7353c7e7cd7540bf2e5906d6173eb51c8fea4e0ea38b19",
+                "sha256:fbe82af9b94ccced752527c8de07fa20267f9634b48674ba478a0bb4000a0b1e"
+            ],
+            "index": "pypi",
+            "version": "==3.5.1"
+        },
+        "docker-pycreds": {
+            "hashes": [
+                "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4",
+                "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49"
+            ],
+            "version": "==0.4.0"
+        },
+        "gitdb2": {
+            "hashes": [
+                "sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2",
+                "sha256:e3a0141c5f2a3f635c7209d56c496ebe1ad35da82fe4d3ec4aaa36278d70648a"
+            ],
+            "version": "==2.0.5"
+        },
+        "gitpython": {
+            "hashes": [
+                "sha256:563221e5a44369c6b79172f455584c9ebbb122a13368cc82cb4b5addff788f82",
+                "sha256:8237dc5bfd6f1366abeee5624111b9d6879393d84745a507de0fda86043b65a8"
+            ],
+            "index": "pypi",
+            "version": "==2.1.11"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
+                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+            ],
+            "version": "==2.8"
+        },
+        "more-itertools": {
+            "hashes": [
+                "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
+                "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
+                "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
+            ],
+            "version": "==5.0.0"
+        },
+        "pluggy": {
+            "hashes": [
+                "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
+                "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
+            ],
+            "version": "==0.8.1"
+        },
+        "py": {
+            "hashes": [
+                "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
+                "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
+            ],
+            "version": "==1.7.0"
+        },
+        "pytest": {
+            "hashes": [
+                "sha256:41568ea7ecb4a68d7f63837cf65b92ce8d0105e43196ff2b26622995bb3dc4b2",
+                "sha256:c3c573a29d7c9547fb90217ece8a8843aa0c1328a797e200290dc3d0b4b823be"
+            ],
+            "index": "pypi",
+            "version": "==4.1.1"
+        },
+        "requests": {
+            "hashes": [
+                "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
+                "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
+            ],
+            "version": "==2.21.0"
+        },
+        "six": {
+            "hashes": [
+                "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
+                "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
+            ],
+            "version": "==1.12.0"
+        },
+        "smmap2": {
+            "hashes": [
+                "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde",
+                "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"
+            ],
+            "version": "==2.0.5"
+        },
+        "urllib3": {
+            "hashes": [
+                "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
+                "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
+            ],
+            "version": "==1.24.1"
+        },
+        "websocket-client": {
+            "hashes": [
+                "sha256:8c8bf2d4f800c3ed952df206b18c28f7070d9e3dcbd6ca6291127574f57ee786",
+                "sha256:e51562c91ddb8148e791f0155fdb01325d99bb52c4cdbb291aee7a3563fd0849"
+            ],
+            "version": "==0.54.0"
+        }
+    },
+    "develop": {}
+}
diff --git a/README.md b/README.md
index 5aa1b87..8f2d72b 100644
--- a/README.md
+++ b/README.md
@@ -117,3 +117,45 @@
 
 * Install a [MySQL slave](helm-charts/gerrit-slave/docs/mysqld.md)
 * Install a [Gerrit slave](helm-charts/gerrit-slave/README.md)
+
+# Running tests
+
+The tests are implemented using Python and `pytest`. To ensure a well-defined
+test-environment, `pipenv` is meant to be used to install packages and provide a
+virtual environment in which to run the tests. To install pipenv, use `brew`:
+
+```sh
+brew install pipenv
+```
+
+More detailed information can be found in the
+[pipenv GitHub repo](https://github.com/pypa/pipenv).
+
+To create the virtual environment with all required packages, run:
+
+```sh
+pipenv install
+```
+
+To run all tests, execute:
+
+```sh
+pipenv run pytest
+```
+
+To run specific tests, execute one of the following:
+
+```sh
+# Run all tests in a directory (including subdirectories)
+pipenv run pytest tests/container-images/base
+
+# Run all tests in a file
+pipenv run pytest tests/container-images/base/test_container_build_base.py
+
+# Run a specific test
+pipenv run \
+  pytest tests/container-images/base/test_container_build_base.py::test_build_base
+```
+
+For a more detailed description of how to use `pytest`, refer to the
+[official documentation](https://docs.pytest.org/en/latest/contents.html).
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..0c3ff81
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# 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.
+
+import os
+
+import docker
+import git
+import pytest
+
+@pytest.fixture(scope="session")
+def docker_client():
+  return docker.from_env()
+
+@pytest.fixture(scope="session")
+def repository_root():
+  git_repo = git.Repo(".", search_parent_directories=True)
+  return git_repo.git.rev_parse("--show-toplevel")
+
+@pytest.fixture(scope="session")
+def container_images(repository_root):
+  image_paths = dict()
+  for directory in os.listdir(os.path.join(repository_root, "container-images")):
+    image_paths[directory] = os.path.join(
+      repository_root,
+      "container-images",
+      directory)
+  return image_paths
+
+@pytest.fixture(scope="session")
+def docker_build(docker_client):
+
+  def docker_build(image, tag):
+    build = docker_client.images.build(
+      path=image,
+      nocache=True,
+      rm=True,
+      tag=tag)
+    return build[0]
+
+  return docker_build
+
+@pytest.fixture(scope="session")
+def base_image(container_images, docker_build):
+  return docker_build(container_images["base"], "base")
diff --git a/tests/container-images/base/test_container_build_base.py b/tests/container-images/base/test_container_build_base.py
new file mode 100644
index 0000000..d10bb27
--- /dev/null
+++ b/tests/container-images/base/test_container_build_base.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# 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.
+
+def test_build_base(base_image):
+  assert base_image.id is not None
diff --git a/tests/container-images/base/test_container_structure_base.py b/tests/container-images/base/test_container_structure_base.py
new file mode 100755
index 0000000..69dec54
--- /dev/null
+++ b/tests/container-images/base/test_container_structure_base.py
@@ -0,0 +1,54 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# 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.
+
+import pytest
+
+@pytest.fixture(scope="module")
+def container_run(request, docker_client, base_image):
+  print("Starting base-container...")
+  container_run = docker_client.containers.run(
+    image=base_image.id,
+    command="tail -f /dev/null",
+    detach=True,
+    auto_remove=True
+  )
+
+  def stop_container():
+    print("Stopping base-container...")
+    container_run.stop(timeout=1)
+
+  request.addfinalizer(stop_container)
+
+  return container_run
+
+
+def test_base_contains_git(container_run):
+  exit_code, _ = container_run.exec_run(
+    "which git"
+  )
+  assert exit_code == 0
+
+def test_base_has_non_root_user_gerrit(container_run):
+  exit_code, output = container_run.exec_run(
+    "id -u gerrit"
+  )
+  assert exit_code == 0
+  uid = int(output.strip().decode("utf-8"))
+  assert uid != 0
+
+def test_base_gerrit_no_root_permissions(container_run):
+  exit_code, _ = container_run.exec_run(
+    "su -c 'rm -rf /bin' gerrit"
+  )
+  assert exit_code > 0