Add test for gerrit-master chart creating and cloning a repo

This change adds integration tests for the gerrit-master chart, which
test whether creation of a project via the Rest API and the subsequent
cloning of this project works. These tests should validate that basic
Gerrit functionality is working using the helm chart to set up a Gerrit
deployment.

Change-Id: I288578ed77f823043703fe66c610a98bb60a20dc
diff --git a/helm-charts/gerrit-master/templates/gerrit-master.stateful-set.yaml b/helm-charts/gerrit-master/templates/gerrit-master.stateful-set.yaml
index 2f6d344..fdca7ed 100644
--- a/helm-charts/gerrit-master/templates/gerrit-master.stateful-set.yaml
+++ b/helm-charts/gerrit-master/templates/gerrit-master.stateful-set.yaml
@@ -21,6 +21,9 @@
     metadata:
       labels:
         app: gerrit-master
+        chart: {{ template "gerrit-master.chart" . }}
+        heritage: {{ .Release.Service }}
+        release: {{ .Release.Name }}
     spec:
       securityContext:
         fsGroup: 100
diff --git a/tests/helm-charts/conftest.py b/tests/helm-charts/conftest.py
index 02c8f05..26f4390 100644
--- a/tests/helm-charts/conftest.py
+++ b/tests/helm-charts/conftest.py
@@ -104,6 +104,7 @@
 
         self.current_context = None
         self.helm = None
+        self.namespaces = list()
 
     def _load_kube_config(self):
         config.load_kube_config(config_file=self.kube_config)
@@ -159,7 +160,7 @@
             else:
                 raise exc
 
-    def _create_image_pull_secret(self):
+    def create_image_pull_secret(self, namespace="default"):
         secret_metadata = client.V1ObjectMeta(name="image-pull-secret")
         auth_string = str.encode(
             "%s:%s" % (self.registry["user"], self.registry["pwd"])
@@ -181,7 +182,7 @@
         )
         core_v1 = client.CoreV1Api()
         try:
-            core_v1.create_namespaced_secret("default", secret_body)
+            core_v1.create_namespaced_secret(namespace, secret_body)
         except client.rest.ApiException as exc:
             if exc.status == 409 and exc.reason == "Conflict":
                 warnings.warn(
@@ -190,6 +191,21 @@
             else:
                 raise exc
 
+    def create_namespace(self, name):
+        namespace_metadata = client.V1ObjectMeta(name=name)
+        namespace_body = client.V1Namespace(
+            kind="Namespace", api_version="v1", metadata=namespace_metadata
+        )
+        core_v1 = client.CoreV1Api()
+        core_v1.create_namespace(body=namespace_body)
+        self.namespaces.append(name)
+        self.create_image_pull_secret(name)
+
+    def delete_namespace(self, name):
+        core_v1 = client.CoreV1Api()
+        core_v1.delete_namespace(name, body=client.V1DeleteOptions())
+        self.namespaces.remove(name)
+
     def init_helm(self):
         self._create_and_deploy_helm_crb()
         self._create_and_deploy_helm_service_account()
@@ -225,7 +241,7 @@
 
     def setup(self):
         self._load_kube_config()
-        self._create_image_pull_secret()
+        self.create_image_pull_secret()
         self.init_helm()
         self.install_storage_provisioner()
 
@@ -237,6 +253,8 @@
         core_v1.delete_namespaced_secret(
             "image-pull-secret", "default", body=client.V1DeleteOptions()
         )
+        while self.namespaces:
+            self.delete_namespace(self.namespaces[0])
 
 
 @pytest.fixture(scope="session")
diff --git a/tests/helm-charts/gerrit-master/conftest.py b/tests/helm-charts/gerrit-master/conftest.py
index 19b5abc..510a4b2 100644
--- a/tests/helm-charts/gerrit-master/conftest.py
+++ b/tests/helm-charts/gerrit-master/conftest.py
@@ -15,9 +15,16 @@
 # limitations under the License.
 
 import os.path
+import time
 
 import pytest
 
+from kubernetes import client
+
+import utils
+
+GERRIT_MASTER_STARTUP_TIMEOUT = 240
+
 
 @pytest.fixture(scope="module")
 def gerrit_master_deployment(
@@ -30,17 +37,55 @@
     gerrit_init_image,
 ):
     chart_path = os.path.join(repository_root, "helm-charts", "gerrit-master")
-    chart_name = "gerrit-master"
+    chart_name = "gerrit-master-" + utils.create_random_string()
     chart_opts = {
         "images.registry.name": request.config.getoption("--registry"),
         "images.version": docker_tag,
         "gerritMaster.ingress.host": "master.%s"
         % request.config.getoption("--ingress-url"),
     }
+    namespace_name = utils.create_random_string()
+    test_cluster.create_namespace(namespace_name)
     test_cluster.helm.install(
-        chart_path, chart_name, set_values=chart_opts, fail_on_err=True
+        chart_path,
+        chart_name,
+        set_values=chart_opts,
+        fail_on_err=True,
+        namespace=namespace_name,
     )
 
-    yield
+    yield {"name": chart_name, "namespace": namespace_name}
 
     test_cluster.helm.delete(chart_name)
+    test_cluster.delete_namespace(namespace_name)
+
+
+@pytest.fixture(scope="module")
+def gerrit_master_ready_deployment(gerrit_master_deployment):
+    def wait_for_readiness():
+        pod_labels = "app=gerrit-master,release=%s" % gerrit_master_deployment["name"]
+        core_v1 = client.CoreV1Api()
+        pod_list = core_v1.list_pod_for_all_namespaces(
+            watch=False, label_selector=pod_labels
+        )
+        for condition in pod_list.items[0].status.conditions:
+            if condition.type == "Ready" and condition.status == "True":
+                return True
+        return False
+
+    timeout = time.time() + GERRIT_MASTER_STARTUP_TIMEOUT
+
+    finished_in_time = False
+
+    while time.time() <= timeout:
+        finished_in_time = wait_for_readiness()
+        if finished_in_time:
+            break
+
+    if not finished_in_time:
+        raise utils.TimeOutException(
+            "Gerrit master pod was not ready in time (%d s)."
+            % GERRIT_MASTER_STARTUP_TIMEOUT
+        )
+
+    yield finished_in_time
diff --git a/tests/helm-charts/gerrit-master/test_chart_gerrit_master_usage.py b/tests/helm-charts/gerrit-master/test_chart_gerrit_master_usage.py
new file mode 100644
index 0000000..e7e376e
--- /dev/null
+++ b/tests/helm-charts/gerrit-master/test_chart_gerrit_master_usage.py
@@ -0,0 +1,54 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2019 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.path
+
+import git
+import pytest
+import requests
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritMasterChartSetup:
+    @pytest.mark.timeout(240)
+    def test_create_project_rest(
+        self, request, test_cluster, gerrit_master_ready_deployment
+    ):
+        create_project_url = "http://master.%s/a/projects/test" % (
+            request.config.getoption("--ingress-url")
+        )
+        response = None
+
+        while not response:
+            try:
+                response = requests.put(create_project_url, auth=("admin", "secret"))
+            except requests.exceptions.ConnectionError:
+                continue
+
+        assert response.status_code == 201
+
+    def test_cloning_project(
+        self, request, tmp_path_factory, test_cluster, gerrit_master_ready_deployment
+    ):
+        clone_dest = tmp_path_factory.mktemp("gerrit_master_chart_clone_test")
+        repo_url = "http://master.%s/test.git" % (
+            request.config.getoption("--ingress-url")
+        )
+        repo = git.Repo.clone_from(repo_url, clone_dest)
+        assert repo.git_dir == os.path.join(clone_dest, ".git")
diff --git a/tests/helpers/helm.py b/tests/helpers/helm.py
index 2c7aadc..28ed2f3 100644
--- a/tests/helpers/helm.py
+++ b/tests/helpers/helm.py
@@ -20,11 +20,11 @@
     def __init__(self, kubeconfig, kubecontext):
         """Wrapper for Helm CLI.
 
-    Arguments:
-      kubeconfig {str} -- Path to kubeconfig-file describing the cluster to
-                          connect to.
-      kubecontext {str} -- Name of the context to use.
-    """
+  Arguments:
+    kubeconfig {str} -- Path to kubeconfig-file describing the cluster to
+                        connect to.
+    kubecontext {str} -- Name of the context to use.
+  """
 
         self.kubeconfig = kubeconfig
         self.kubecontext = kubecontext
@@ -61,7 +61,15 @@
         helm_cmd = ["init", "--wait", "--service-account", serviceaccount]
         return self._exec_command(helm_cmd)
 
-    def install(self, chart, name, values_file=None, set_values=None, fail_on_err=True):
+    def install(
+        self,
+        chart,
+        name,
+        values_file=None,
+        set_values=None,
+        namespace=None,
+        fail_on_err=True,
+    ):
         """Installs a chart on the cluster
 
     Arguments:
@@ -73,6 +81,7 @@
       set_values {dict} -- Dictionary containing key-value-pairs that are used
                            to overwrite values in the values.yaml-file.
                            (default: {None})
+      namespace {str} -- Namespace to install the release into (default: {default})
       fail_on_err {bool} -- Whether to fail with an exception if the installation
                             fails (default: {True})
 
@@ -88,6 +97,8 @@
         if set_values:
             opt_list = ["%s=%s" % (k, v) for k, v in set_values.items()]
             helm_cmd.extend(("--set", ",".join(opt_list)))
+        if namespace:
+            helm_cmd.extend(("--namespace", namespace))
         return self._exec_command(helm_cmd, fail_on_err)
 
     def list(self):
diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py
index 8a74927..7b9bf72 100644
--- a/tests/helpers/utils.py
+++ b/tests/helpers/utils.py
@@ -12,11 +12,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import random
+import string
+
+
+class TimeOutException(Exception):
+    """ Exception to be raised, if some action does not finish in time. """
+
 
 def check_if_ancestor_image_is_inherited(image, ancestor):
     """Helper function that looks for a given ancestor image in the layers of a
-     provided image. It can be used to check, whether an image uses the expected
-     FROM-statement
+    provided image. It can be used to check, whether an image uses the expected
+    FROM-statement
 
   Arguments:
     image {docker.images.Image} -- Docker image object to be checked
@@ -32,3 +39,7 @@
         if contains_tag:
             break
     return contains_tag
+
+
+def create_random_string(length=8):
+    return "".join([random.choice(string.ascii_letters) for n in range(length)]).lower()