update charts
diff --git a/charts/k8s-gerrit/tests/conftest.py b/charts/k8s-gerrit/tests/conftest.py
new file mode 100644
index 0000000..eefb8f9
--- /dev/null
+++ b/charts/k8s-gerrit/tests/conftest.py
@@ -0,0 +1,337 @@
+# pylint: disable=W0613, W0212
+
+# 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 argparse
+import getpass
+import os
+import sys
+
+from pathlib import Path
+
+import docker
+import pygit2 as git
+import pytest
+
+sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
+
+# pylint: disable=C0103
+pytest_plugins = ["fixtures.credentials", "fixtures.cluster", "fixtures.helm.gerrit"]
+
+# Base images that are not published and thus only tagged with "latest"
+BASE_IMGS = ["base", "gerrit-base"]
+
+
+# pylint: disable=W0622
+class PasswordPromptAction(argparse.Action):
+ def __init__(
+ self,
+ option_strings,
+ dest=None,
+ nargs=0,
+ default=None,
+ required=False,
+ type=None,
+ metavar=None,
+ help=None,
+ ):
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=nargs,
+ default=default,
+ required=required,
+ metavar=metavar,
+ type=type,
+ help=help,
+ )
+
+ def __call__(self, parser, args, values, option_string=None):
+ password = getpass.getpass()
+ setattr(args, self.dest, password)
+
+
+def pytest_addoption(parser):
+ parser.addoption(
+ "--registry",
+ action="store",
+ default="",
+ help="Container registry to push (if --push=true) and pull container images"
+ + "from for tests on Kubernetes clusters (default: '')",
+ )
+ parser.addoption(
+ "--registry-user",
+ action="store",
+ default="",
+ help="Username for container registry (default: '')",
+ )
+ parser.addoption(
+ "--registry-pwd",
+ action="store",
+ default="",
+ help="Password for container registry (default: '')",
+ )
+ parser.addoption(
+ "--org",
+ action="store",
+ default="k8sgerrit",
+ help="Docker organization (default: 'k8sgerrit')",
+ )
+ parser.addoption(
+ "--push",
+ action="store_true",
+ help="If set, the docker images will be pushed to the registry configured"
+ + "by --registry (default: false)",
+ )
+ parser.addoption(
+ "--tag",
+ action="store",
+ default=None,
+ help="Tag of cached container images to test. Missing images will be built."
+ + "(default: All container images will be built)",
+ )
+ parser.addoption(
+ "--build-cache",
+ action="store_true",
+ help="If set, the docker cache will be used when building container images.",
+ )
+ parser.addoption(
+ "--kubeconfig",
+ action="store",
+ default=None,
+ help="Kubeconfig to use for cluster connection. If none is given the currently"
+ + "configured context is used.",
+ )
+ parser.addoption(
+ "--rwm-storageclass",
+ action="store",
+ default="shared-storage",
+ help="Name of the storageclass used for ReadWriteMany access."
+ + "(default: shared-storage)",
+ )
+ parser.addoption(
+ "--ingress-url",
+ action="store",
+ default=None,
+ help="URL of the ingress domain used by the cluster.",
+ )
+ parser.addoption(
+ "--gerrit-user",
+ action="store",
+ default="admin",
+ help="Gerrit admin username to be used for smoke tests. (default: admin)",
+ )
+ parser.addoption(
+ "--gerrit-pwd",
+ action=PasswordPromptAction,
+ default="secret",
+ help="Gerrit admin password to be used for smoke tests. (default: secret)",
+ )
+ parser.addoption(
+ "--skip-slow", action="store_true", help="If set, skip slow tests."
+ )
+
+
+def pytest_collection_modifyitems(config, items):
+ if config.getoption("--skip-slow"):
+ skip_slow = pytest.mark.skip(reason="--skip-slow was set.")
+ for item in items:
+ if "slow" in item.keywords:
+ item.add_marker(skip_slow)
+
+
+def pytest_runtest_makereport(item, call):
+ if "incremental" in item.keywords:
+ if call.excinfo is not None:
+ parent = item.parent
+ parent._previousfailed = item
+
+
+def pytest_runtest_setup(item):
+ if "incremental" in item.keywords:
+ previousfailed = getattr(item.parent, "_previousfailed", None)
+ if previousfailed is not None:
+ pytest.xfail(f"previous test failed ({previousfailed.name})")
+
+
+@pytest.fixture(scope="session")
+def tag_of_cached_container(request):
+ return request.config.getoption("--tag")
+
+
+@pytest.fixture(scope="session")
+def docker_client():
+ return docker.from_env()
+
+
+@pytest.fixture(scope="session")
+def repository_root():
+ return Path(git.discover_repository(os.path.realpath(__file__))).parent.absolute()
+
+
+@pytest.fixture(scope="session")
+def container_images(repository_root):
+ image_paths = {}
+ 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_registry(request):
+ registry = request.config.getoption("--registry")
+ if registry and not registry[-1] == "/":
+ registry += "/"
+ return registry
+
+
+@pytest.fixture(scope="session")
+def docker_org(request):
+ org = request.config.getoption("--org")
+ if org and not org[-1] == "/":
+ org += "/"
+ return org
+
+
+@pytest.fixture(scope="session")
+def docker_tag(tag_of_cached_container, repository_root):
+ if tag_of_cached_container:
+ return tag_of_cached_container
+ return git.Repository(repository_root).describe(dirty_suffix="-dirty")
+
+
+@pytest.fixture(scope="session")
+def docker_build(
+ request,
+ docker_client,
+ tag_of_cached_container,
+ docker_registry,
+ docker_org,
+ docker_tag,
+):
+ def docker_build(image, name):
+ if name in BASE_IMGS:
+ image_name = f"{name}:latest"
+ else:
+ image_name = f"{docker_registry}{docker_org}{name}:{docker_tag}"
+
+ if tag_of_cached_container:
+ try:
+ return docker_client.images.get(image_name)
+ except docker.errors.ImageNotFound:
+ print(f"Image {image_name} could not be loaded. Building it now.")
+
+ no_cache = not request.config.getoption("--build-cache")
+
+ build = docker_client.images.build(
+ path=image,
+ nocache=no_cache,
+ rm=True,
+ tag=image_name,
+ platform="linux/amd64",
+ )
+ return build[0]
+
+ return docker_build
+
+
+@pytest.fixture(scope="session")
+def docker_login(request, docker_client, docker_registry):
+ username = request.config.getoption("--registry-user")
+ if username:
+ docker_client.login(
+ username=username,
+ password=request.config.getoption("--registry-pwd"),
+ registry=docker_registry,
+ )
+
+
+@pytest.fixture(scope="session")
+def docker_push(
+ request, docker_client, docker_registry, docker_login, docker_org, docker_tag
+):
+ def docker_push(image):
+ docker_repository = f"{docker_registry}{docker_org}{image}"
+ docker_client.images.push(docker_repository, tag=docker_tag)
+
+ return docker_push
+
+
+@pytest.fixture(scope="session")
+def docker_network(request, docker_client):
+ network = docker_client.networks.create(
+ name="k8sgerrit-test-network", scope="local"
+ )
+
+ yield network
+
+ network.remove()
+
+
+@pytest.fixture(scope="session")
+def base_image(container_images, docker_build):
+ return docker_build(container_images["base"], "base")
+
+
+@pytest.fixture(scope="session")
+def gerrit_base_image(container_images, docker_build, base_image):
+ return docker_build(container_images["gerrit-base"], "gerrit-base")
+
+
+@pytest.fixture(scope="session")
+def gitgc_image(request, container_images, docker_build, docker_push, base_image):
+ gitgc_image = docker_build(container_images["git-gc"], "git-gc")
+ if request.config.getoption("--push"):
+ docker_push("git-gc")
+ return gitgc_image
+
+
+@pytest.fixture(scope="session")
+def apache_git_http_backend_image(
+ request, container_images, docker_build, docker_push, base_image
+):
+ apache_git_http_backend_image = docker_build(
+ container_images["apache-git-http-backend"], "apache-git-http-backend"
+ )
+ if request.config.getoption("--push"):
+ docker_push("apache-git-http-backend")
+ return apache_git_http_backend_image
+
+
+@pytest.fixture(scope="session")
+def gerrit_image(
+ request, container_images, docker_build, docker_push, base_image, gerrit_base_image
+):
+ gerrit_image = docker_build(container_images["gerrit"], "gerrit")
+ if request.config.getoption("--push"):
+ docker_push("gerrit")
+ return gerrit_image
+
+
+@pytest.fixture(scope="session")
+def gerrit_init_image(
+ request, container_images, docker_build, docker_push, base_image, gerrit_base_image
+):
+ gerrit_init_image = docker_build(container_images["gerrit-init"], "gerrit-init")
+ if request.config.getoption("--push"):
+ docker_push("gerrit-init")
+ return gerrit_init_image
+
+
+@pytest.fixture(scope="session")
+def required_plugins(request):
+ return ["healthcheck"]
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/conftest.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/conftest.py
new file mode 100644
index 0000000..8cd3443
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/conftest.py
@@ -0,0 +1,92 @@
+# pylint: disable=W0613
+
+# 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 random
+import string
+import time
+
+import pytest
+
+
+class GitBackendContainer:
+ def __init__(self, docker_client, image, port, credentials_dir):
+ self.docker_client = docker_client
+ self.image = image
+ self.port = port
+ self.apache_credentials_dir = credentials_dir
+
+ self.container = None
+
+ def start(self):
+ self.container = self.docker_client.containers.run(
+ image=self.image.id,
+ ports={"80": self.port},
+ volumes={
+ self.apache_credentials_dir: {
+ "bind": "/var/apache/credentials",
+ "mode": "ro",
+ }
+ },
+ detach=True,
+ auto_remove=True,
+ platform="linux/amd64",
+ )
+
+ def stop(self):
+ self.container.stop(timeout=1)
+
+
+@pytest.fixture(scope="module")
+def container_run_factory(
+ docker_client, apache_git_http_backend_image, htpasswd, credentials_dir
+):
+ def run_container(port):
+ return GitBackendContainer(
+ docker_client,
+ apache_git_http_backend_image,
+ port,
+ str(credentials_dir),
+ )
+
+ return run_container
+
+
+@pytest.fixture(scope="module")
+def container_run(container_run_factory, free_port):
+ test_setup = container_run_factory(free_port)
+ test_setup.start()
+ time.sleep(3)
+
+ yield test_setup
+
+ test_setup.stop()
+
+
+@pytest.fixture(scope="module")
+def base_url(container_run):
+ return f"http://localhost:{container_run.port}"
+
+
+@pytest.fixture(scope="function")
+def random_repo_name():
+ return "".join(
+ [random.choice(string.ascii_letters + string.digits) for n in range(8)]
+ )
+
+
+@pytest.fixture(scope="function")
+def repo_creation_url(base_url, random_repo_name):
+ return f"{base_url}/a/projects/{random_repo_name}"
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_build_apache_git_http_backend.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_build_apache_git_http_backend.py
new file mode 100644
index 0000000..984d6be
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_build_apache_git_http_backend.py
@@ -0,0 +1,24 @@
+# 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.mark.structure
+def test_build_apache_git_http_backend_image(
+ apache_git_http_backend_image, tag_of_cached_container
+):
+ if tag_of_cached_container:
+ pytest.skip("Cached image used for testing. Build will not be tested.")
+ assert apache_git_http_backend_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_integration_apache_git_http_backend.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_integration_apache_git_http_backend.py
new file mode 100755
index 0000000..0d5ef65
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_integration_apache_git_http_backend.py
@@ -0,0 +1,96 @@
+# pylint: disable=W0613
+
+# 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.
+
+from pathlib import Path
+
+import os.path
+
+import pygit2 as git
+import pytest
+import requests
+
+
+@pytest.fixture(scope="function")
+def repo_dir(tmp_path_factory, random_repo_name):
+ return tmp_path_factory.mktemp(random_repo_name)
+
+
+@pytest.fixture(scope="function")
+def mock_repo(repo_dir):
+ repo = git.init_repository(repo_dir, False)
+ file_name = os.path.join(repo_dir, "test.txt")
+ Path(file_name).touch()
+ repo.index.add("test.txt")
+ repo.index.write()
+ # pylint: disable=E1101
+ author = git.Signature("Gerrit Review", "gerrit@review.com")
+ committer = git.Signature("Gerrit Review", "gerrit@review.com")
+ message = "Initial commit"
+ tree = repo.index.write_tree()
+ repo.create_commit("HEAD", author, committer, message, tree, [])
+ return repo
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation(
+ container_run, htpasswd, repo_creation_url
+):
+ request = requests.put(
+ repo_creation_url,
+ auth=requests.auth.HTTPBasicAuth(htpasswd["user"], htpasswd["password"]),
+ )
+ assert request.status_code == 201
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation_fails_without_credentials(
+ container_run, repo_creation_url
+):
+ request = requests.put(repo_creation_url)
+ assert request.status_code == 401
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation_fails_wrong_fs_permissions(
+ container_run, htpasswd, repo_creation_url
+):
+ container_run.container.exec_run("chown -R root:root /var/gerrit/git")
+ request = requests.put(
+ repo_creation_url,
+ auth=requests.auth.HTTPBasicAuth(htpasswd["user"], htpasswd["password"]),
+ )
+ container_run.container.exec_run("chown -R gerrit:users /var/gerrit/git")
+ assert request.status_code == 500
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation_push_repo(
+ container_run, base_url, htpasswd, mock_repo, random_repo_name
+):
+ container_run.container.exec_run(
+ f"su -c 'git init --bare /var/gerrit/git/{random_repo_name}.git' gerrit"
+ )
+ url = f"{base_url}/{random_repo_name}.git"
+ url = url.replace("//", f"//{htpasswd['user']}:{htpasswd['password']}@")
+ origin = mock_repo.remotes.create("origin", url)
+ origin.push(["refs/heads/master:refs/heads/master"])
+
+ remote_refs = origin.ls_remotes()
+ assert str(remote_refs[0]["oid"]) == mock_repo.revparse_single("HEAD").hex
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_structure_apache_git_http_backend.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_structure_apache_git_http_backend.py
new file mode 100755
index 0000000..a138ef5
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_structure_apache_git_http_backend.py
@@ -0,0 +1,61 @@
+# 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
+import utils
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_apache_git_http_backend_inherits_from_base(apache_git_http_backend_image):
+ assert utils.check_if_ancestor_image_is_inherited(
+ apache_git_http_backend_image, "base:latest"
+ )
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_contains_apache2(container_run):
+ exit_code, _ = container_run.container.exec_run("which httpd")
+ assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_http_site_configured(container_run):
+ exit_code, _ = container_run.container.exec_run(
+ "test -f /etc/apache2/conf.d/git-http-backend.conf"
+ )
+ assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_contains_start_script(container_run):
+ exit_code, _ = container_run.container.exec_run("test -f /var/tools/start")
+ assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_contains_repo_creation_cgi_script(container_run):
+ exit_code, _ = container_run.container.exec_run("test -f /var/cgi/project_admin.sh")
+ assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_apache_git_http_backend_has_entrypoint(apache_git_http_backend_image):
+ entrypoint = apache_git_http_backend_image.attrs["ContainerConfig"]["Entrypoint"]
+ assert len(entrypoint) == 2
+ assert entrypoint[1] == "/var/tools/start"
diff --git a/charts/k8s-gerrit/tests/container-images/base/test_container_build_base.py b/charts/k8s-gerrit/tests/container-images/base/test_container_build_base.py
new file mode 100644
index 0000000..2a3afa5
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/base/test_container_build_base.py
@@ -0,0 +1,22 @@
+# 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.mark.structure
+def test_build_base(base_image, tag_of_cached_container):
+ if tag_of_cached_container:
+ pytest.skip("Cached image used for testing. Build will not be tested.")
+ assert base_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/base/test_container_structure_base.py b/charts/k8s-gerrit/tests/container-images/base/test_container_structure_base.py
new file mode 100755
index 0000000..528d2b4
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/base/test_container_structure_base.py
@@ -0,0 +1,45 @@
+# 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(docker_client, container_endless_run_factory, base_image):
+ container_run = container_endless_run_factory(docker_client, base_image)
+ yield container_run
+ container_run.stop(timeout=1)
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_base_contains_git(container_run):
+ exit_code, _ = container_run.exec_run("which git")
+ assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+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
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+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
diff --git a/charts/k8s-gerrit/tests/container-images/conftest.py b/charts/k8s-gerrit/tests/container-images/conftest.py
new file mode 100644
index 0000000..8a4b8f2
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/conftest.py
@@ -0,0 +1,105 @@
+# 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.path
+import socket
+
+import pytest
+
+
+class GerritContainer:
+ def __init__(self, docker_client, docker_network, tmp_dir, image, configs, port):
+ self.docker_client = docker_client
+ self.docker_network = docker_network
+ self.tmp_dir = tmp_dir
+ self.image = image
+ self.configs = configs
+ self.port = port
+
+ self.container = None
+
+ def _create_config_files(self):
+ tmp_config_dir = os.path.join(self.tmp_dir, "configs")
+ if not os.path.isdir(tmp_config_dir):
+ os.mkdir(tmp_config_dir)
+ config_paths = {}
+ for filename, content in self.configs.items():
+ gerrit_config_file = os.path.join(tmp_config_dir, filename)
+ with open(gerrit_config_file, "w", encoding="utf-8") as config_file:
+ config_file.write(content)
+ config_paths[filename] = gerrit_config_file
+ return config_paths
+
+ def _define_volume_mounts(self):
+ volumes = {
+ v: {"bind": f"/var/gerrit/etc/{k}", "mode": "rw"}
+ for (k, v) in self._create_config_files().items()
+ }
+ volumes[os.path.join(self.tmp_dir, "lib")] = {
+ "bind": "/var/gerrit/lib",
+ "mode": "rw",
+ }
+ return volumes
+
+ def start(self):
+ self.container = self.docker_client.containers.run(
+ image=self.image.id,
+ user="gerrit",
+ volumes=self._define_volume_mounts(),
+ ports={8080: str(self.port)},
+ network=self.docker_network.name,
+ detach=True,
+ auto_remove=True,
+ platform="linux/amd64",
+ )
+
+ def stop(self):
+ self.container.stop(timeout=1)
+
+
+@pytest.fixture(scope="session")
+def gerrit_container_factory():
+ def get_gerrit_container(
+ docker_client, docker_network, tmp_dir, image, gerrit_config, port
+ ):
+ return GerritContainer(
+ docker_client, docker_network, tmp_dir, image, gerrit_config, port
+ )
+
+ return get_gerrit_container
+
+
+@pytest.fixture(scope="session")
+def container_endless_run_factory():
+ def get_container(docker_client, image):
+ return docker_client.containers.run(
+ image=image.id,
+ entrypoint="/bin/ash",
+ command=["-c", "tail -f /dev/null"],
+ user="gerrit",
+ detach=True,
+ auto_remove=True,
+ platform="linux/amd64",
+ )
+
+ return get_container
+
+
+@pytest.fixture(scope="session")
+def free_port():
+ skt = socket.socket()
+ skt.bind(("", 0))
+ port = skt.getsockname()[1]
+ skt.close()
+ return port
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_build_gerrit_base.py b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_build_gerrit_base.py
new file mode 100644
index 0000000..93954d8
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_build_gerrit_base.py
@@ -0,0 +1,22 @@
+# 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.mark.structure
+def test_build_gerrit_base(gerrit_base_image, tag_of_cached_container):
+ if tag_of_cached_container:
+ pytest.skip("Cached image used for testing. Build will not be tested.")
+ assert gerrit_base_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_structure_gerrit_base.py b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_structure_gerrit_base.py
new file mode 100755
index 0000000..05161b2
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_structure_gerrit_base.py
@@ -0,0 +1,100 @@
+# 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 re
+
+import pytest
+
+import utils
+
+
+JAVA_VER = 11
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gerrit_base_image):
+ container_run = container_endless_run_factory(docker_client, gerrit_base_image)
+ yield container_run
+ container_run.stop(timeout=1)
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gerrit_base_inherits_from_base(gerrit_base_image):
+ assert utils.check_if_ancestor_image_is_inherited(gerrit_base_image, "base:latest")
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_contains_java(container_run):
+ _, output = container_run.exec_run("java -version")
+ output = output.strip().decode("utf-8")
+ assert re.search(re.compile(f'openjdk version "{JAVA_VER}.[0-9.]+"'), output)
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_java_path(container_run):
+ exit_code, output = container_run.exec_run(
+ '/bin/ash -c "readlink -f $(which java)"'
+ )
+ output = output.strip().decode("utf-8")
+ assert exit_code == 0
+ assert output == f"/usr/lib/jvm/java-{JAVA_VER}-openjdk/bin/java"
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_contains_gerrit_war(container_run):
+ exit_code, _ = container_run.exec_run("test -f /var/war/gerrit.war")
+ assert exit_code == 0
+
+ exit_code, _ = container_run.exec_run("test -f /var/gerrit/bin/gerrit.war")
+ assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_war_contains_gerrit(container_run):
+ exit_code, output = container_run.exec_run("java -jar /var/war/gerrit.war version")
+ assert exit_code == 0
+ output = output.strip().decode("utf-8")
+ assert re.search(re.compile("gerrit version.*"), output)
+
+ exit_code, output = container_run.exec_run(
+ "java -jar /var/gerrit/bin/gerrit.war version"
+ )
+ assert exit_code == 0
+ output = output.strip().decode("utf-8")
+ assert re.search(re.compile("gerrit version.*"), output)
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_site_permissions(container_run):
+ exit_code, _ = container_run.exec_run("test -O /var/gerrit")
+ assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_war_dir_permissions(container_run):
+ exit_code, _ = container_run.exec_run("test -O /var/war")
+ assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_gerrit_base_has_entrypoint(gerrit_base_image):
+ entrypoint = gerrit_base_image.attrs["ContainerConfig"]["Entrypoint"]
+ assert "/var/tools/start" in entrypoint
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_build_gerrit_init.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_build_gerrit_init.py
new file mode 100644
index 0000000..dc16d74
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_build_gerrit_init.py
@@ -0,0 +1,22 @@
+# 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.mark.structure
+def test_build_gerrit_init(gerrit_init_image, tag_of_cached_container):
+ if tag_of_cached_container:
+ pytest.skip("Cached image used for testing. Build will not be tested.")
+ assert gerrit_init_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init.py
new file mode 100644
index 0000000..4dac6e0
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init.py
@@ -0,0 +1,190 @@
+# pylint: disable=E1101
+
+# 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.path
+import re
+
+from docker.errors import NotFound
+
+import pytest
+import yaml
+
+
+@pytest.fixture(scope="class")
+def container_run_default(request, docker_client, gerrit_init_image, tmp_path_factory):
+ tmp_site_dir = tmp_path_factory.mktemp("gerrit_site")
+ container_run = docker_client.containers.run(
+ image=gerrit_init_image.id,
+ user="gerrit",
+ volumes={tmp_site_dir: {"bind": "/var/gerrit", "mode": "rw"}},
+ detach=True,
+ auto_remove=True,
+ platform="linux/amd64",
+ )
+
+ def stop_container():
+ try:
+ container_run.stop(timeout=1)
+ except NotFound:
+ print("Container already stopped.")
+
+ request.addfinalizer(stop_container)
+
+ return container_run
+
+
+@pytest.fixture(scope="class")
+def init_config_dir(tmp_path_factory):
+ return tmp_path_factory.mktemp("init_config")
+
+
+@pytest.fixture(scope="class")
+def tmp_site_dir(tmp_path_factory):
+ return tmp_path_factory.mktemp("gerrit_site")
+
+
+@pytest.fixture(scope="class")
+def container_run_endless(
+ docker_client, gerrit_init_image, init_config_dir, tmp_site_dir
+):
+ container_run = docker_client.containers.run(
+ image=gerrit_init_image.id,
+ entrypoint="/bin/ash",
+ command=["-c", "tail -f /dev/null"],
+ user="gerrit",
+ volumes={
+ tmp_site_dir: {"bind": "/var/gerrit", "mode": "rw"},
+ init_config_dir: {"bind": "/var/config", "mode": "rw"},
+ },
+ detach=True,
+ auto_remove=True,
+ platform="linux/amd64",
+ )
+
+ yield container_run
+ container_run.stop(timeout=1)
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+class TestGerritInitEmptySite:
+ @pytest.mark.timeout(60)
+ def test_gerrit_init_gerrit_is_initialized(self, container_run_default):
+ def wait_for_init_success_message():
+ log = container_run_default.logs().decode("utf-8")
+ return log, re.search(r"Initialized /var/gerrit", log)
+
+ while not wait_for_init_success_message():
+ continue
+
+ @pytest.mark.timeout(60)
+ def test_gerrit_init_exits_after_init(self, container_run_default):
+ assert container_run_default.wait()["StatusCode"] == 0
+
+
+@pytest.fixture(
+ scope="function",
+ params=[
+ ["replication", "reviewnotes"],
+ ["replication", "reviewnotes", "hooks"],
+ ["download-commands"],
+ [],
+ ],
+)
+def plugins_to_install(request):
+ return request.param
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+class TestGerritInitPluginInstallation:
+ def _configure_packaged_plugins(self, file_path, plugins):
+ with open(file_path, "w", encoding="utf-8") as f:
+ yaml.dump(
+ {"plugins": [{"name": p} for p in plugins]}, f, default_flow_style=False
+ )
+
+ def test_gerrit_init_plugins_are_installed(
+ self,
+ container_run_endless,
+ init_config_dir,
+ plugins_to_install,
+ tmp_site_dir,
+ required_plugins,
+ ):
+ self._configure_packaged_plugins(
+ os.path.join(init_config_dir, "init.yaml"), plugins_to_install
+ )
+
+ exit_code, _ = container_run_endless.exec_run(
+ "python3 /var/tools/gerrit-initializer -s /var/gerrit -c /var/config/init.yaml init"
+ )
+ assert exit_code == 0
+
+ plugins_path = os.path.join(tmp_site_dir, "plugins")
+
+ for plugin in plugins_to_install:
+ assert os.path.exists(os.path.join(plugins_path, f"{plugin}.jar"))
+
+ installed_plugins = os.listdir(plugins_path)
+ expected_plugins = plugins_to_install + required_plugins
+ for plugin in installed_plugins:
+ assert os.path.splitext(plugin)[0] in expected_plugins
+
+ def test_required_plugins_are_installed(
+ self, container_run_endless, init_config_dir, tmp_site_dir, required_plugins
+ ):
+ self._configure_packaged_plugins(
+ os.path.join(init_config_dir, "init.yaml"), ["hooks"]
+ )
+
+ exit_code, _ = container_run_endless.exec_run(
+ "python3 /var/tools/gerrit-initializer -s /var/gerrit -c /var/config/init.yaml init"
+ )
+ assert exit_code == 0
+
+ for plugin in required_plugins:
+ assert os.path.exists(
+ os.path.join(tmp_site_dir, "plugins", f"{plugin}.jar")
+ )
+
+ def test_libraries_are_symlinked(
+ self, container_run_endless, init_config_dir, tmp_site_dir
+ ):
+ with open(
+ os.path.join(init_config_dir, "init.yaml"), "w", encoding="utf-8"
+ ) as f:
+ yaml.dump(
+ {"plugins": [{"name": "hooks", "installAsLibrary": True}]},
+ f,
+ default_flow_style=False,
+ )
+
+ exit_code, _ = container_run_endless.exec_run(
+ "python3 /var/tools/gerrit-initializer -s /var/gerrit -c /var/config/init.yaml init"
+ )
+ assert exit_code == 0
+
+ assert os.path.exists(os.path.join(tmp_site_dir, "plugins", "hooks.jar"))
+ assert os.path.islink(os.path.join(tmp_site_dir, "lib", "hooks.jar"))
+
+ exit_code, output = container_run_endless.exec_run(
+ "readlink -f /var/gerrit/lib/hooks.jar"
+ )
+ assert exit_code == 0
+ assert output.decode("utf-8").strip() == "/var/gerrit/plugins/hooks.jar"
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init_reindexing.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init_reindexing.py
new file mode 100644
index 0000000..c8a5b49
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init_reindexing.py
@@ -0,0 +1,160 @@
+# pylint: disable=E1101
+
+# 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 pytest
+
+
+@pytest.fixture(scope="function")
+def temp_site(tmp_path_factory):
+ return tmp_path_factory.mktemp("gerrit-index-test")
+
+
+@pytest.fixture(scope="function")
+def container_run_endless(request, docker_client, gerrit_init_image, temp_site):
+ container_run = docker_client.containers.run(
+ image=gerrit_init_image.id,
+ entrypoint="/bin/ash",
+ command=["-c", "tail -f /dev/null"],
+ volumes={str(temp_site): {"bind": "/var/gerrit", "mode": "rw"}},
+ user="gerrit",
+ detach=True,
+ auto_remove=True,
+ platform="linux/amd64",
+ )
+
+ def stop_container():
+ container_run.stop(timeout=1)
+
+ request.addfinalizer(stop_container)
+
+ return container_run
+
+
+@pytest.mark.incremental
+class TestGerritReindex:
+ def _get_indices(self, container):
+ _, indices = container.exec_run(
+ "git config -f /var/gerrit/index/gerrit_index.config "
+ + "--name-only "
+ + "--get-regexp index"
+ )
+ indices = indices.decode().strip().splitlines()
+ return [index.split(".")[1] for index in indices]
+
+ def test_gerrit_init_skips_reindexing_on_fresh_site(
+ self, temp_site, container_run_endless
+ ):
+ assert not os.path.exists(
+ os.path.join(temp_site, "index", "gerrit_index.config")
+ )
+ exit_code, _ = container_run_endless.exec_run(
+ (
+ "python3 /var/tools/gerrit-initializer "
+ "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+ )
+ )
+ assert exit_code == 0
+ expected_files = ["gerrit_index.config"] + self._get_indices(
+ container_run_endless
+ )
+ for expected_file in expected_files:
+ assert os.path.exists(os.path.join(temp_site, "index", expected_file))
+
+ timestamp_index_dir = os.path.getctime(os.path.join(temp_site, "index"))
+
+ exit_code, _ = container_run_endless.exec_run(
+ (
+ "python3 /var/tools/gerrit-initializer "
+ "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+ )
+ )
+ assert exit_code == 0
+ assert timestamp_index_dir == os.path.getctime(os.path.join(temp_site, "index"))
+
+ def test_gerrit_init_fixes_missing_index_config(
+ self, container_run_endless, temp_site
+ ):
+ container_run_endless.exec_run(
+ (
+ "python3 /var/tools/gerrit-initializer "
+ "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+ )
+ )
+ os.remove(os.path.join(temp_site, "index", "gerrit_index.config"))
+
+ exit_code, _ = container_run_endless.exec_run(
+ (
+ "python3 /var/tools/gerrit-initializer "
+ "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+ )
+ )
+ assert exit_code == 0
+
+ exit_code, _ = container_run_endless.exec_run("/var/gerrit/bin/gerrit.sh start")
+ assert exit_code == 0
+
+ def test_gerrit_init_fixes_not_ready_indices(self, container_run_endless):
+ container_run_endless.exec_run(
+ (
+ "python3 /var/tools/gerrit-initializer "
+ "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+ )
+ )
+
+ indices = self._get_indices(container_run_endless)
+ assert indices
+ container_run_endless.exec_run(
+ f"git config -f /var/gerrit/index/gerrit_index.config {indices[0]} false"
+ )
+
+ exit_code, _ = container_run_endless.exec_run(
+ (
+ "python3 /var/tools/gerrit-initializer "
+ "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+ )
+ )
+ assert exit_code == 0
+
+ exit_code, _ = container_run_endless.exec_run("/var/gerrit/bin/gerrit.sh start")
+ assert exit_code == 0
+
+ def test_gerrit_init_fixes_outdated_indices(self, container_run_endless, temp_site):
+ container_run_endless.exec_run(
+ (
+ "python3 /var/tools/gerrit-initializer "
+ "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+ )
+ )
+
+ index = self._get_indices(container_run_endless)[0]
+ (name, version) = index.split("_")
+ os.rename(
+ os.path.join(temp_site, "index", index),
+ os.path.join(temp_site, "index", f"{name}_{int(version) - 1:04d}"),
+ )
+
+ exit_code, _ = container_run_endless.exec_run(
+ (
+ "python3 /var/tools/gerrit-initializer "
+ "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+ )
+ )
+ assert exit_code == 0
+
+ exit_code, _ = container_run_endless.exec_run("/var/gerrit/bin/gerrit.sh start")
+ assert exit_code == 0
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py
new file mode 100755
index 0000000..5861a5e
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py
@@ -0,0 +1,74 @@
+# 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
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gerrit_init_image):
+ container_run = container_endless_run_factory(docker_client, gerrit_init_image)
+ yield container_run
+ container_run.stop(timeout=1)
+
+
+@pytest.fixture(
+ scope="function",
+ params=[
+ "/var/tools/gerrit-initializer/__main__.py",
+ "/var/tools/gerrit-initializer/main.py",
+ ],
+)
+def expected_script(request):
+ return request.param
+
+
+@pytest.fixture(scope="function", params=["python3"])
+def expected_tool(request):
+ return request.param
+
+
+@pytest.fixture(scope="function", params=["pyyaml", "requests"])
+def expected_pip_package(request):
+ return request.param
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gerrit_init_inherits_from_gerrit_base(gerrit_init_image):
+ assert utils.check_if_ancestor_image_is_inherited(
+ gerrit_init_image, "gerrit-base:latest"
+ )
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_init_contains_expected_scripts(container_run, expected_script):
+ exit_code, _ = container_run.exec_run(f"test -f {expected_script}")
+ assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_init_contains_expected_tools(container_run, expected_tool):
+ exit_code, _ = container_run.exec_run(f"which {expected_tool}")
+ assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_gerrit_init_has_entrypoint(gerrit_init_image):
+ entrypoint = gerrit_init_image.attrs["ContainerConfig"]["Entrypoint"]
+ assert len(entrypoint) >= 1
+ assert entrypoint == ["python3", "/var/tools/gerrit-initializer"]
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_build_gerrit.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_build_gerrit.py
new file mode 100644
index 0000000..a2c3dd5
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_build_gerrit.py
@@ -0,0 +1,22 @@
+# 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.mark.structure
+def test_build_gerrit(gerrit_image, tag_of_cached_container):
+ if tag_of_cached_container:
+ pytest.skip("Cached image used for testing. Build will not be tested.")
+ assert gerrit_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit.py
new file mode 100644
index 0000000..9376a4a
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit.py
@@ -0,0 +1,108 @@
+# pylint: disable=W0613, E1101
+
+# 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 re
+import time
+
+import pytest
+import requests
+
+
+@pytest.fixture(scope="module")
+def tmp_dir(tmp_path_factory):
+ return tmp_path_factory.mktemp("gerrit-test")
+
+
+@pytest.fixture(scope="class")
+def container_run(
+ docker_client,
+ docker_network,
+ tmp_dir,
+ gerrit_image,
+ gerrit_container_factory,
+ free_port,
+):
+ configs = {
+ "gerrit.config": """
+ [gerrit]
+ basePath = git
+
+ [httpd]
+ listenUrl = http://*:8080
+
+ [test]
+ success = True
+ """,
+ "secure.config": """
+ [test]
+ success = True
+ """,
+ "replication.config": """
+ [test]
+ success = True
+ """,
+ }
+ test_setup = gerrit_container_factory(
+ docker_client, docker_network, tmp_dir, gerrit_image, configs, free_port
+ )
+ test_setup.start()
+
+ yield test_setup
+
+ test_setup.stop()
+
+
+@pytest.fixture(params=["gerrit.config", "secure.config", "replication.config"])
+def config_file_to_test(request):
+ return request.param
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.slow
+class TestGerritStartScript:
+ @pytest.mark.timeout(60)
+ def test_gerrit_gerrit_starts_up(self, container_run):
+ def wait_for_gerrit_start():
+ log = container_run.container.logs().decode("utf-8")
+ return re.search(r"Gerrit Code Review .+ ready", log)
+
+ while not wait_for_gerrit_start:
+ continue
+
+ def test_gerrit_custom_gerrit_config_available(
+ self, container_run, config_file_to_test
+ ):
+ exit_code, output = container_run.container.exec_run(
+ f"git config --file=/var/gerrit/etc/{config_file_to_test} --get test.success"
+ )
+ output = output.decode("utf-8").strip()
+ assert exit_code == 0
+ assert output == "True"
+
+ @pytest.mark.timeout(60)
+ def test_gerrit_httpd_is_responding(self, container_run):
+ status = None
+ while not status == 200:
+ try:
+ response = requests.get(f"http://localhost:{container_run.port}")
+ status = response.status_code
+ except requests.exceptions.ConnectionError:
+ time.sleep(1)
+
+ assert response.status_code == 200
+ assert re.search(r'content="Gerrit Code Review"', response.text)
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit_replica.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit_replica.py
new file mode 100644
index 0000000..3673eab
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit_replica.py
@@ -0,0 +1,122 @@
+# pylint: disable=W0613, E1101
+
+# 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 os.path
+import re
+
+import pygit2 as git
+import pytest
+import requests
+
+CONFIG_FILES = ["gerrit.config", "secure.config"]
+
+
+@pytest.fixture(scope="module")
+def tmp_dir(tmp_path_factory):
+ return tmp_path_factory.mktemp("gerrit-replica-test")
+
+
+@pytest.fixture(scope="class")
+def container_run(
+ request,
+ docker_client,
+ docker_network,
+ tmp_dir,
+ gerrit_image,
+ gerrit_container_factory,
+ free_port,
+):
+ configs = {
+ "gerrit.config": """
+ [gerrit]
+ basePath = git
+
+ [httpd]
+ listenUrl = http://*:8080
+
+ [container]
+ replica = true
+
+ [test]
+ success = True
+ """,
+ "secure.config": """
+ [test]
+ success = True
+ """,
+ }
+
+ test_setup = gerrit_container_factory(
+ docker_client, docker_network, tmp_dir, gerrit_image, configs, free_port
+ )
+ test_setup.start()
+
+ request.addfinalizer(test_setup.stop)
+
+ return test_setup
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.slow
+class TestGerritReplica:
+ @pytest.fixture(params=CONFIG_FILES)
+ def config_file_to_test(self, request):
+ return request.param
+
+ @pytest.fixture(params=["All-Users.git", "All-Projects.git"])
+ def expected_repository(self, request):
+ return request.param
+
+ @pytest.mark.timeout(60)
+ def test_gerrit_replica_gerrit_starts_up(self, container_run):
+ def wait_for_gerrit_start():
+ log = container_run.container.logs().decode("utf-8")
+ return re.search(r"Gerrit Code Review .+ ready", log)
+
+ while not wait_for_gerrit_start():
+ continue
+
+ def test_gerrit_replica_custom_gerrit_config_available(
+ self, container_run, config_file_to_test
+ ):
+ exit_code, output = container_run.container.exec_run(
+ f"git config --file=/var/gerrit/etc/{config_file_to_test} --get test.success"
+ )
+ output = output.decode("utf-8").strip()
+ assert exit_code == 0
+ assert output == "True"
+
+ def test_gerrit_replica_repository_exists(self, container_run, expected_repository):
+ exit_code, _ = container_run.container.exec_run(
+ f"test -d /var/gerrit/git/{expected_repository}"
+ )
+ assert exit_code == 0
+
+ def test_gerrit_replica_clone_repo_works(self, container_run, tmp_path_factory):
+ container_run.container.exec_run("git init --bare /var/gerrit/git/test.git")
+ clone_dest = tmp_path_factory.mktemp("gerrit_replica_clone_test")
+ repo = git.clone_repository(
+ f"http://localhost:{container_run.port}/test.git", clone_dest
+ )
+ assert repo.path == os.path.join(clone_dest, ".git/")
+
+ def test_gerrit_replica_webui_not_accessible(self, container_run):
+ response = requests.get(f"http://localhost:{container_run.port}")
+ assert response.status_code == 404
+ assert response.text == "Not Found"
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_structure_gerrit.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_structure_gerrit.py
new file mode 100755
index 0000000..7ece25e
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_structure_gerrit.py
@@ -0,0 +1,39 @@
+# 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
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gerrit_image):
+ container_run = container_endless_run_factory(docker_client, gerrit_image)
+ yield container_run
+ container_run.stop(timeout=1)
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gerrit_inherits_from_gerrit_base(gerrit_image):
+ assert utils.check_if_ancestor_image_is_inherited(
+ gerrit_image, "gerrit-base:latest"
+ )
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_contains_start_script(container_run):
+ exit_code, _ = container_run.exec_run("test -f /var/tools/start")
+ assert exit_code == 0
diff --git a/charts/k8s-gerrit/tests/container-images/git-gc/test_container_build_gitgc.py b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_build_gitgc.py
new file mode 100644
index 0000000..a640d20
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_build_gitgc.py
@@ -0,0 +1,22 @@
+# 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.mark.structure
+def test_build_gitgc(gitgc_image, tag_of_cached_container):
+ if tag_of_cached_container:
+ pytest.skip("Cached image used for testing. Build will not be tested.")
+ assert gitgc_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/git-gc/test_container_structure_gitgc.py b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_structure_gitgc.py
new file mode 100644
index 0000000..9f03644
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_structure_gitgc.py
@@ -0,0 +1,51 @@
+# 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
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gitgc_image):
+ container_run = container_endless_run_factory(docker_client, gitgc_image)
+ yield container_run
+ container_run.stop(timeout=1)
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gitgc_inherits_from_base(gitgc_image):
+ assert utils.check_if_ancestor_image_is_inherited(gitgc_image, "base:latest")
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gitgc_log_dir_writable_by_gerrit(container_run):
+ exit_code, _ = container_run.exec_run("touch /var/log/git/test.log")
+ assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gitgc_contains_gc_script(container_run):
+ exit_code, _ = container_run.exec_run("test -f /var/tools/gc.sh")
+ assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_gitgc_has_entrypoint(gitgc_image):
+ entrypoint = gitgc_image.attrs["ContainerConfig"]["Entrypoint"]
+ assert len(entrypoint) == 1
+ assert entrypoint[0] == "/var/tools/gc.sh"
diff --git a/charts/k8s-gerrit/tests/fixtures/__init__.py b/charts/k8s-gerrit/tests/fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/__init__.py
diff --git a/charts/k8s-gerrit/tests/fixtures/cluster.py b/charts/k8s-gerrit/tests/fixtures/cluster.py
new file mode 100644
index 0000000..eb94968
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/cluster.py
@@ -0,0 +1,144 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2022 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 base64
+import json
+import warnings
+
+from kubernetes import client, config
+
+import pytest
+
+from .helm.client import HelmClient
+
+
+class Cluster:
+ def __init__(self, kube_config):
+ self.kube_config = kube_config
+
+ self.image_pull_secrets = []
+ self.namespaces = []
+
+ context = self._load_kube_config()
+ self.helm = HelmClient(self.kube_config, context)
+
+ def _load_kube_config(self):
+ config.load_kube_config(config_file=self.kube_config)
+ _, context = config.list_kube_config_contexts(config_file=self.kube_config)
+ return context["name"]
+
+ def _apply_image_pull_secrets(self, namespace):
+ for ips in self.image_pull_secrets:
+ try:
+ client.CoreV1Api().create_namespaced_secret(namespace, ips)
+ except client.rest.ApiException as exc:
+ if exc.status == 409 and exc.reason == "Conflict":
+ warnings.warn(
+ "Kubernetes Cluster not empty. Image pull secret already exists."
+ )
+ else:
+ raise exc
+
+ def add_container_registry(self, secret_name, url, user, pwd):
+ data = {
+ "auths": {
+ url: {
+ "auth": base64.b64encode(str.encode(f"{user}:{pwd}")).decode(
+ "utf-8"
+ )
+ }
+ }
+ }
+ metadata = client.V1ObjectMeta(name=secret_name)
+ self.image_pull_secrets.append(
+ client.V1Secret(
+ api_version="v1",
+ kind="Secret",
+ metadata=metadata,
+ type="kubernetes.io/dockerconfigjson",
+ data={
+ ".dockerconfigjson": base64.b64encode(
+ json.dumps(data).encode()
+ ).decode("utf-8")
+ },
+ )
+ )
+
+ def create_namespace(self, name):
+ namespace_metadata = client.V1ObjectMeta(name=name)
+ namespace_body = client.V1Namespace(
+ kind="Namespace", api_version="v1", metadata=namespace_metadata
+ )
+ client.CoreV1Api().create_namespace(body=namespace_body)
+ self.namespaces.append(name)
+ self._apply_image_pull_secrets(name)
+
+ def delete_namespace(self, name):
+ if name not in self.namespaces:
+ return
+
+ client.CoreV1Api().delete_namespace(name, body=client.V1DeleteOptions())
+ self.namespaces.remove(name)
+
+ def cleanup(self):
+ while self.namespaces:
+ self.helm.delete_all(
+ namespace=self.namespaces[0],
+ )
+ self.delete_namespace(self.namespaces[0])
+
+
+@pytest.fixture(scope="session")
+def test_cluster(request):
+ kube_config = request.config.getoption("--kubeconfig")
+
+ test_cluster = Cluster(kube_config)
+ test_cluster.add_container_registry(
+ "image-pull-secret",
+ request.config.getoption("--registry"),
+ request.config.getoption("--registry-user"),
+ request.config.getoption("--registry-pwd"),
+ )
+
+ yield test_cluster
+
+ test_cluster.cleanup()
+
+
+@pytest.fixture(scope="session")
+def ldap_credentials(test_cluster):
+ ldap_secret = client.CoreV1Api().read_namespaced_secret(
+ "openldap-users", namespace="openldap"
+ )
+ users = base64.b64decode(ldap_secret.data["users"]).decode("utf-8").split(",")
+ passwords = (
+ base64.b64decode(ldap_secret.data["passwords"]).decode("utf-8").split(",")
+ )
+ credentials = {}
+ for i, user in enumerate(users):
+ credentials[user] = passwords[i]
+
+ yield credentials
+
+
+@pytest.fixture(scope="session")
+def ldap_admin_credentials(test_cluster):
+ ldap_secret = client.CoreV1Api().read_namespaced_secret(
+ "openldap-admin", namespace="openldap"
+ )
+ password = base64.b64decode(ldap_secret.data["adminpassword"]).decode("utf-8")
+
+ yield ("admin", password)
diff --git a/charts/k8s-gerrit/tests/fixtures/credentials.py b/charts/k8s-gerrit/tests/fixtures/credentials.py
new file mode 100644
index 0000000..de39dc1
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/credentials.py
@@ -0,0 +1,39 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2022 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 pytest
+
+from passlib.apache import HtpasswdFile
+
+import utils
+
+
+@pytest.fixture(scope="session")
+def credentials_dir(tmp_path_factory):
+ return tmp_path_factory.mktemp("creds")
+
+
+@pytest.fixture(scope="session")
+def htpasswd(credentials_dir):
+ basic_auth_creds = {"user": "admin", "password": utils.create_random_string(16)}
+ htpasswd_file = HtpasswdFile(os.path.join(credentials_dir, ".htpasswd"), new=True)
+ htpasswd_file.set_password(basic_auth_creds["user"], basic_auth_creds["password"])
+ htpasswd_file.save()
+ basic_auth_creds["htpasswd_string"] = htpasswd_file.to_string()
+ basic_auth_creds["htpasswd_file"] = credentials_dir
+ yield basic_auth_creds
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/__init__.py b/charts/k8s-gerrit/tests/fixtures/helm/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/__init__.py
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/abstract_deployment.py b/charts/k8s-gerrit/tests/fixtures/helm/abstract_deployment.py
new file mode 100644
index 0000000..517cfe2
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/abstract_deployment.py
@@ -0,0 +1,99 @@
+import abc
+import random
+import re
+import string
+
+from time import time
+
+from kubernetes import client
+
+
+class AbstractDeployment(abc.ABC):
+ def __init__(self, tmp_dir):
+ self.tmp_dir = tmp_dir
+ self.namespace = "".join(
+ [random.choice(string.ascii_letters) for n in range(8)]
+ ).lower()
+ self.values_file = self._set_values_file()
+ self.chart_opts = {}
+
+ @abc.abstractmethod
+ def install(self, wait=True):
+ pass
+
+ @abc.abstractmethod
+ def update(self):
+ pass
+
+ @abc.abstractmethod
+ def uninstall(self):
+ pass
+
+ @abc.abstractmethod
+ def _set_values_file(self):
+ pass
+
+ def set_helm_value(self, combined_key, value):
+ nested_keys = re.split(r"(?<!\\)\.", combined_key)
+ dct_pointer = self.chart_opts
+ for key in nested_keys[:-1]:
+ # pylint: disable=W1401
+ key.replace("\.", ".")
+ dct_pointer = dct_pointer.setdefault(key, {})
+ # pylint: disable=W1401
+ dct_pointer[nested_keys[-1].replace("\.", ".")] = value
+
+ def _wait_for_pod_readiness(self, pod_labels, timeout=180):
+ """Helper function that can be used to wait for all pods with a given set of
+ labels to be ready.
+
+ Arguments:
+ pod_labels {str} -- Label selector string to be used to select pods.
+ (https://kubernetes.io/docs/concepts/overview/working-with-objects/\
+ labels/#label-selectors)
+
+ Keyword Arguments:
+ timeout {int} -- Time in seconds to wait for the pod status to become ready.
+ (default: {180})
+
+ Returns:
+ boolean -- Whether pods were ready in time.
+ """
+
+ def check_pod_readiness():
+ core_v1 = client.CoreV1Api()
+ pod_list = core_v1.list_pod_for_all_namespaces(
+ watch=False, label_selector=pod_labels
+ )
+ for pod in pod_list.items:
+ for condition in pod.status.conditions:
+ if condition.type != "Ready" and condition.status != "True":
+ return False
+ return True
+
+ return self._exec_fn_with_timeout(check_pod_readiness, limit=timeout)
+
+ def _exec_fn_with_timeout(self, func, limit=60):
+ """Helper function that executes a given function until it returns True or a
+ given time limit is reached.
+
+ Arguments:
+ func {function} -- Function to execute. The function can return some output
+ (or None) and as a second return value a boolean indicating,
+ whether the event the function was waiting for has happened.
+
+ Keyword Arguments:
+ limit {int} -- Maximum time in seconds to wait for a positive response of
+ the function (default: {60})
+
+ Returns:
+ boolean -- False, if the timeout was reached
+ any -- Last output of fn
+ """
+
+ timeout = time() + limit
+ while time() < timeout:
+ is_finished = func()
+ if is_finished:
+ return True
+ return False
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/client.py b/charts/k8s-gerrit/tests/fixtures/helm/client.py
new file mode 100644
index 0000000..eb3285f
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/client.py
@@ -0,0 +1,202 @@
+# 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 json
+import subprocess
+
+
+class HelmClient:
+ 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.
+ """
+
+ self.kubeconfig = kubeconfig
+ self.kubecontext = kubecontext
+
+ def _exec_command(self, cmd, fail_on_err=True):
+ base_cmd = [
+ "helm",
+ "--kubeconfig",
+ self.kubeconfig,
+ "--kube-context",
+ self.kubecontext,
+ ]
+ return subprocess.run(
+ base_cmd + cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=fail_on_err,
+ text=True,
+ )
+
+ def install(
+ self,
+ chart,
+ name,
+ values_file=None,
+ set_values=None,
+ namespace=None,
+ fail_on_err=True,
+ wait=True,
+ ):
+ """Installs a chart on the cluster
+
+ Arguments:
+ chart {str} -- Release name or path of a helm chart
+ name {str} -- Name with which the chart will be installed on the cluster
+
+ Keyword Arguments:
+ values_file {str} -- Path to a custom values.yaml file (default: {None})
+ 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})
+ wait {bool} -- Whether to wait for all pods to be ready (default: {True})
+
+ Returns:
+ CompletedProcess -- CompletedProcess-object returned by subprocess
+ containing details about the result and output of the
+ executed command.
+ """
+
+ helm_cmd = ["install", name, chart, "--dependency-update"]
+ if values_file:
+ helm_cmd.extend(("-f", values_file))
+ if set_values:
+ opt_list = [f"{k}={v}" for k, v in set_values.items()]
+ helm_cmd.extend(("--set", ",".join(opt_list)))
+ if namespace:
+ helm_cmd.extend(("--namespace", namespace))
+ if wait:
+ helm_cmd.append("--wait")
+ return self._exec_command(helm_cmd, fail_on_err)
+
+ def list(self, namespace=None):
+ """Lists helm charts installed on the cluster.
+
+ Keyword Arguments:
+ namespace {str} -- Kubernetes namespace (default: {None})
+
+ Returns:
+ list -- List of helm chart realeases installed on the cluster.
+ """
+
+ helm_cmd = ["list", "--all", "--output", "json"]
+ if namespace:
+ helm_cmd.extend(("--namespace", namespace))
+ output = self._exec_command(helm_cmd).stdout
+ return json.loads(output)
+
+ def upgrade(
+ self,
+ chart,
+ name,
+ namespace,
+ values_file=None,
+ set_values=None,
+ reuse_values=True,
+ fail_on_err=True,
+ ):
+ """Updates a chart on the cluster
+
+ Arguments:
+ chart {str} -- Release name or path of a helm chart
+ name {str} -- Name with which the chart will be installed on the cluster
+ namespace {str} -- Kubernetes namespace
+
+ Keyword Arguments:
+ values_file {str} -- Path to a custom values.yaml file (default: {None})
+ set_values {dict} -- Dictionary containing key-value-pairs that are used
+ to overwrite values in the values.yaml-file.
+ (default: {None})
+ reuse_values {bool} -- Whether to reuse existing not overwritten values
+ (default: {True})
+ fail_on_err {bool} -- Whether to fail with an exception if the installation
+ fails (default: {True})
+
+ Returns:
+ CompletedProcess -- CompletedProcess-object returned by subprocess
+ containing details about the result and output of the
+ executed command.
+ """
+ helm_cmd = ["upgrade", name, chart, "--namespace", namespace, "--wait"]
+ if values_file:
+ helm_cmd.extend(("-f", values_file))
+ if reuse_values:
+ helm_cmd.append("--reuse-values")
+ if set_values:
+ opt_list = [f"{k}={v}" for k, v in set_values.items()]
+ helm_cmd.extend(("--set", ",".join(opt_list)))
+ return self._exec_command(helm_cmd, fail_on_err)
+
+ def delete(self, name, namespace=None):
+ """Deletes a chart from the cluster
+
+ Arguments:
+ name {str} -- Name of the chart to delete
+
+ Keyword Arguments:
+ namespace {str} -- Kubernetes namespace (default: {None})
+
+ Returns:
+ CompletedProcess -- CompletedProcess-object returned by subprocess
+ containing details about the result and output of
+ the executed command.
+ """
+
+ if name not in self.list(namespace):
+ return None
+
+ helm_cmd = ["delete", name]
+ if namespace:
+ helm_cmd.extend(("--namespace", namespace))
+ return self._exec_command(helm_cmd)
+
+ def delete_all(self, namespace=None, exceptions=None):
+ """Deletes all charts on the cluster
+
+ Keyword Arguments:
+ namespace {str} -- Kubernetes namespace (default: {None})
+ exceptions {list} -- List of chart names not to delete (default: {None})
+ """
+
+ charts = self.list(namespace)
+ for chart in charts:
+ if exceptions and chart["name"] in exceptions:
+ continue
+ self.delete(chart["name"], namespace)
+
+ def is_installed(self, namespace, chart):
+ """Checks if a chart is installed in the cluster
+
+ Keyword Arguments:
+ namespace {str} -- Kubernetes namespace
+ chart {str} -- Name of the chart
+
+ Returns:
+ bool -- Whether the chart is installed
+ """
+
+ for installed_chart in self.list(namespace):
+ if installed_chart["name"] == chart:
+ return True
+
+ return False
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/gerrit.py b/charts/k8s-gerrit/tests/fixtures/helm/gerrit.py
new file mode 100644
index 0000000..ec7a7c1
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/gerrit.py
@@ -0,0 +1,279 @@
+# Copyright (C) 2022 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
+
+from copy import deepcopy
+from pathlib import Path
+
+import pytest
+import yaml
+
+import pygit2 as git
+import chromedriver_autoinstaller
+from kubernetes import client
+from selenium import webdriver
+from selenium.webdriver.common.by import By
+
+from .abstract_deployment import AbstractDeployment
+
+
+class TimeOutException(Exception):
+ """Exception to be raised, if some action does not finish in time."""
+
+
+def dict_to_git_config(config_dict):
+ config = ""
+ for section, options in config_dict.items():
+ config += f"[{section}]\n"
+ for key, value in options.items():
+ if isinstance(value, bool):
+ value = "true" if value else "false"
+ elif isinstance(value, list):
+ for opt in value:
+ config += f" {key} = {opt}\n"
+ continue
+ config += f" {key} = {value}\n"
+ return config
+
+
+GERRIT_STARTUP_TIMEOUT = 240
+
+DEFAULT_GERRIT_CONFIG = {
+ "auth": {
+ "type": "LDAP",
+ },
+ "container": {
+ "user": "gerrit",
+ "javaHome": "/usr/lib/jvm/java-11-openjdk",
+ "javaOptions": [
+ "-Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore",
+ "-Xms200m",
+ "-Xmx4g",
+ ],
+ },
+ "gerrit": {
+ "basePath": "git",
+ "canonicalWebUrl": "http://example.com/",
+ "serverId": "gerrit-1",
+ },
+ "httpd": {
+ "listenUrl": "proxy-https://*:8080/",
+ "requestLog": True,
+ "gracefulStopTimeout": "1m",
+ },
+ "index": {"type": "LUCENE", "onlineUpgrade": False},
+ "ldap": {
+ "server": "ldap://openldap.openldap.svc.cluster.local:1389",
+ "accountbase": "dc=example,dc=org",
+ "username": "cn=admin,dc=example,dc=org",
+ },
+ "sshd": {"listenAddress": "off"},
+}
+
+DEFAULT_VALUES = {
+ "gitRepositoryStorage": {"externalPVC": {"use": True, "name": "repo-storage"}},
+ "gitGC": {"logging": {"persistence": {"enabled": False}}},
+ "gerrit": {
+ "etc": {"config": {"gerrit.config": dict_to_git_config(DEFAULT_GERRIT_CONFIG)}}
+ },
+}
+
+
+# pylint: disable=R0902
+class GerritDeployment(AbstractDeployment):
+ def __init__(
+ self,
+ tmp_dir,
+ cluster,
+ storageclass,
+ container_registry,
+ container_org,
+ container_version,
+ ingress_url,
+ ldap_admin_credentials,
+ ldap_credentials,
+ ):
+ super().__init__(tmp_dir)
+ self.cluster = cluster
+ self.storageclass = storageclass
+ self.ldap_credentials = ldap_credentials
+
+ self.chart_name = "gerrit-" + self.namespace
+ self.chart_path = os.path.join(
+ # pylint: disable=E1101
+ Path(git.discover_repository(os.path.realpath(__file__))).parent.absolute(),
+ "helm-charts",
+ "gerrit",
+ )
+
+ self.gerrit_config = deepcopy(DEFAULT_GERRIT_CONFIG)
+ self.chart_opts = deepcopy(DEFAULT_VALUES)
+
+ self._configure_container_images(
+ container_registry, container_org, container_version
+ )
+ self.hostname = f"{self.namespace}.{ingress_url}"
+ self._configure_ingress()
+ self.set_gerrit_config_value(
+ "gerrit", "canonicalWebUrl", f"http://{self.hostname}"
+ )
+ # pylint: disable=W1401
+ self.set_helm_value(
+ "gerrit.etc.secret.secure\.config",
+ dict_to_git_config({"ldap": {"password": ldap_admin_credentials[1]}}),
+ )
+
+ def install(self, wait=True):
+ if self.cluster.helm.is_installed(self.namespace, self.chart_name):
+ self.update()
+ return
+
+ with open(self.values_file, "w", encoding="UTF-8") as f:
+ yaml.dump(self.chart_opts, f)
+
+ self.cluster.create_namespace(self.namespace)
+ self._create_pvc()
+
+ self.cluster.helm.install(
+ self.chart_path,
+ self.chart_name,
+ values_file=self.values_file,
+ fail_on_err=True,
+ namespace=self.namespace,
+ wait=wait,
+ )
+
+ def create_admin_account(self):
+ self.wait_until_ready()
+ chromedriver_autoinstaller.install()
+ options = webdriver.ChromeOptions()
+ options.add_argument("--headless")
+ options.add_argument("--no-sandbox")
+ options.add_argument("--ignore-certificate-errors")
+ options.set_capability("acceptInsecureCerts", True)
+ driver = webdriver.Chrome(
+ options=options,
+ )
+ driver.get(f"http://{self.hostname}/login")
+ user_input = driver.find_element(By.ID, "f_user")
+ user_input.send_keys("gerrit-admin")
+
+ pwd_input = driver.find_element(By.ID, "f_pass")
+ pwd_input.send_keys(self.ldap_credentials["gerrit-admin"])
+
+ submit_btn = driver.find_element(By.ID, "b_signin")
+ submit_btn.click()
+
+ driver.close()
+
+ def update(self):
+ with open(self.values_file, "w", encoding="UTF-8") as f:
+ yaml.dump(self.chart_opts, f)
+
+ self.cluster.helm.upgrade(
+ self.chart_path,
+ self.chart_name,
+ values_file=self.values_file,
+ fail_on_err=True,
+ namespace=self.namespace,
+ )
+
+ def wait_until_ready(self):
+ pod_labels = f"app=gerrit,release={self.chart_name}"
+ finished_in_time = self._wait_for_pod_readiness(
+ pod_labels, timeout=GERRIT_STARTUP_TIMEOUT
+ )
+
+ if not finished_in_time:
+ raise TimeOutException(
+ f"Gerrit pod was not ready in time ({GERRIT_STARTUP_TIMEOUT} s)."
+ )
+
+ def uninstall(self):
+ self.cluster.helm.delete(self.chart_name, namespace=self.namespace)
+ self.cluster.delete_namespace(self.namespace)
+
+ def set_gerrit_config_value(self, section, key, value):
+ if isinstance(self.gerrit_config[section][key], list):
+ self.gerrit_config[section][key].append(value)
+ else:
+ self.gerrit_config[section][key] = value
+ # pylint: disable=W1401
+ self.set_helm_value(
+ "gerrit.etc.config.gerrit\.config", dict_to_git_config(self.gerrit_config)
+ )
+
+ def _set_values_file(self):
+ return os.path.join(self.tmp_dir, "values.yaml")
+
+ def _configure_container_images(
+ self, container_registry, container_org, container_version
+ ):
+ self.set_helm_value("images.registry.name", container_registry)
+ self.set_helm_value("gitGC.image", f"{container_org}/git-gc")
+ self.set_helm_value("gerrit.images.gerritInit", f"{container_org}/gerrit-init")
+ self.set_helm_value("gerrit.images.gerrit", f"{container_org}/gerrit")
+ self.set_helm_value("images.version", container_version)
+
+ def _configure_ingress(self):
+ self.set_helm_value("ingress.enabled", True)
+ self.set_helm_value("ingress.host", self.hostname)
+
+ def _create_pvc(self):
+ core_v1 = client.CoreV1Api()
+ core_v1.create_namespaced_persistent_volume_claim(
+ self.namespace,
+ body=client.V1PersistentVolumeClaim(
+ kind="PersistentVolumeClaim",
+ api_version="v1",
+ metadata=client.V1ObjectMeta(name="repo-storage"),
+ spec=client.V1PersistentVolumeClaimSpec(
+ access_modes=["ReadWriteMany"],
+ storage_class_name=self.storageclass,
+ resources=client.V1ResourceRequirements(
+ requests={"storage": "1Gi"}
+ ),
+ ),
+ ),
+ )
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment(
+ request, tmp_path_factory, test_cluster, ldap_admin_credentials, ldap_credentials
+):
+ deployment = GerritDeployment(
+ tmp_path_factory.mktemp("gerrit_deployment"),
+ test_cluster,
+ request.config.getoption("--rwm-storageclass").lower(),
+ request.config.getoption("--registry"),
+ request.config.getoption("--org"),
+ request.config.getoption("--tag"),
+ request.config.getoption("--ingress-url"),
+ ldap_admin_credentials,
+ ldap_credentials,
+ )
+
+ yield deployment
+
+ deployment.uninstall()
+
+
+@pytest.fixture(scope="class")
+def default_gerrit_deployment(gerrit_deployment):
+ gerrit_deployment.install()
+ gerrit_deployment.create_admin_account()
+
+ yield gerrit_deployment
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py
new file mode 100644
index 0000000..62981ac
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py
@@ -0,0 +1,343 @@
+# 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 hashlib
+import json
+import os.path
+import time
+
+import pytest
+import requests
+
+from kubernetes import client
+from kubernetes.stream import stream
+
+PLUGINS = ["avatars-gravatar", "readonly"]
+LIBS = ["global-refdb"]
+GERRIT_VERSION = "3.8"
+
+
+@pytest.fixture(scope="module")
+def plugin_list():
+ plugin_list = []
+ for plugin in PLUGINS:
+ url = (
+ f"https://gerrit-ci.gerritforge.com/view/Plugins-stable-{GERRIT_VERSION}/"
+ f"job/plugin-{plugin}-bazel-master-stable-{GERRIT_VERSION}/lastSuccessfulBuild/"
+ f"artifact/bazel-bin/plugins/{plugin}/{plugin}.jar"
+ )
+ jar = requests.get(url, verify=False).content
+ plugin_list.append(
+ {"name": plugin, "url": url, "sha1": hashlib.sha1(jar).hexdigest()}
+ )
+ return plugin_list
+
+
+@pytest.fixture(scope="module")
+def lib_list():
+ lib_list = []
+ for lib in LIBS:
+ url = (
+ f"https://gerrit-ci.gerritforge.com/view/Plugins-stable-{GERRIT_VERSION}/"
+ f"job/module-{lib}-bazel-stable-{GERRIT_VERSION}/lastSuccessfulBuild/"
+ f"artifact/bazel-bin/plugins/{lib}/{lib}.jar"
+ )
+ jar = requests.get(url, verify=False).content
+ lib_list.append(
+ {"name": lib, "url": url, "sha1": hashlib.sha1(jar).hexdigest()}
+ )
+ return lib_list
+
+
+@pytest.fixture(
+ scope="class",
+ params=[
+ [{"name": "replication"}],
+ [{"name": "replication"}, {"name": "download-commands"}],
+ ],
+ ids=["single-packaged-plugin", "multiple-packaged-plugins"],
+)
+def gerrit_deployment_with_packaged_plugins(request, gerrit_deployment):
+ gerrit_deployment.set_helm_value("gerrit.pluginManagement.plugins", request.param)
+ gerrit_deployment.install()
+ gerrit_deployment.create_admin_account()
+
+ yield gerrit_deployment, request.param
+
+
+@pytest.fixture(
+ scope="class", params=[1, 2], ids=["single-other-plugin", "multiple-other-plugins"]
+)
+def gerrit_deployment_with_other_plugins(
+ request,
+ plugin_list,
+ gerrit_deployment,
+):
+ selected_plugins = plugin_list[: request.param]
+
+ gerrit_deployment.set_helm_value(
+ "gerrit.pluginManagement.plugins", selected_plugins
+ )
+
+ gerrit_deployment.install()
+ gerrit_deployment.create_admin_account()
+
+ yield gerrit_deployment, selected_plugins
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment_with_libs(
+ request,
+ lib_list,
+ gerrit_deployment,
+):
+ gerrit_deployment.set_helm_value("gerrit.pluginManagement.libs", lib_list)
+
+ gerrit_deployment.install()
+ gerrit_deployment.create_admin_account()
+
+ yield gerrit_deployment, lib_list
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment_with_other_plugin_wrong_sha(plugin_list, gerrit_deployment):
+ plugin = plugin_list[0]
+ plugin["sha1"] = "notAValidSha"
+ gerrit_deployment.set_helm_value("gerrit.pluginManagement.plugins", [plugin])
+
+ gerrit_deployment.install(wait=False)
+
+ yield gerrit_deployment
+
+
+def get_gerrit_plugin_list(gerrit_url, user="admin", password="secret"):
+ list_plugins_url = f"{gerrit_url}/a/plugins/?all"
+ response = requests.get(list_plugins_url, auth=(user, password))
+ if not response.status_code == 200:
+ return None
+ body = response.text
+ return json.loads(body[body.index("\n") + 1 :])
+
+
+def get_gerrit_lib_list(gerrit_deployment):
+ response = (
+ stream(
+ client.CoreV1Api().connect_get_namespaced_pod_exec,
+ gerrit_deployment.chart_name + "-gerrit-stateful-set-0",
+ gerrit_deployment.namespace,
+ command=["/bin/ash", "-c", "ls /var/gerrit/lib"],
+ stdout=True,
+ )
+ .strip()
+ .split()
+ )
+ return [os.path.splitext(r)[0] for r in response]
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestgerritChartPackagedPluginInstall:
+ def _assert_installed_plugins(self, expected_plugins, installed_plugins):
+ for plugin in expected_plugins:
+ plugin_name = plugin["name"]
+ assert plugin_name in installed_plugins
+ assert installed_plugins[plugin_name]["filename"] == f"{plugin_name}.jar"
+
+ @pytest.mark.timeout(300)
+ def test_install_packaged_plugins(
+ self, request, gerrit_deployment_with_packaged_plugins, ldap_credentials
+ ):
+ gerrit_deployment, expected_plugins = gerrit_deployment_with_packaged_plugins
+ response = None
+ while not response:
+ try:
+ response = get_gerrit_plugin_list(
+ f"http://{gerrit_deployment.hostname}",
+ "gerrit-admin",
+ ldap_credentials["gerrit-admin"],
+ )
+ except requests.exceptions.ConnectionError:
+ time.sleep(1)
+
+ self._assert_installed_plugins(expected_plugins, response)
+
+ @pytest.mark.timeout(300)
+ def test_install_packaged_plugins_are_removed_with_update(
+ self,
+ request,
+ test_cluster,
+ gerrit_deployment_with_packaged_plugins,
+ ldap_credentials,
+ ):
+ gerrit_deployment, expected_plugins = gerrit_deployment_with_packaged_plugins
+ removed_plugin = expected_plugins.pop()
+
+ gerrit_deployment.set_helm_value(
+ "gerrit.pluginManagement.plugins", expected_plugins
+ )
+ gerrit_deployment.update()
+
+ response = None
+ while True:
+ try:
+ response = get_gerrit_plugin_list(
+ f"http://{gerrit_deployment.hostname}",
+ "gerrit-admin",
+ ldap_credentials["gerrit-admin"],
+ )
+ if response is not None and removed_plugin["name"] not in response:
+ break
+ except requests.exceptions.ConnectionError:
+ time.sleep(1)
+
+ assert removed_plugin["name"] not in response
+ self._assert_installed_plugins(expected_plugins, response)
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritChartOtherPluginInstall:
+ def _assert_installed_plugins(self, expected_plugins, installed_plugins):
+ for plugin in expected_plugins:
+ assert plugin["name"] in installed_plugins
+ assert (
+ installed_plugins[plugin["name"]]["filename"] == f"{plugin['name']}.jar"
+ )
+
+ @pytest.mark.timeout(300)
+ def test_install_other_plugins(
+ self, gerrit_deployment_with_other_plugins, ldap_credentials
+ ):
+ gerrit_deployment, expected_plugins = gerrit_deployment_with_other_plugins
+ response = None
+ while not response:
+ try:
+ response = get_gerrit_plugin_list(
+ f"http://{gerrit_deployment.hostname}",
+ "gerrit-admin",
+ ldap_credentials["gerrit-admin"],
+ )
+ except requests.exceptions.ConnectionError:
+ continue
+ self._assert_installed_plugins(expected_plugins, response)
+
+ @pytest.mark.timeout(300)
+ def test_install_other_plugins_are_removed_with_update(
+ self, gerrit_deployment_with_other_plugins, ldap_credentials
+ ):
+ gerrit_deployment, installed_plugins = gerrit_deployment_with_other_plugins
+ removed_plugin = installed_plugins.pop()
+ gerrit_deployment.set_helm_value(
+ "gerrit.pluginManagement.plugins", installed_plugins
+ )
+ gerrit_deployment.update()
+
+ response = None
+ while True:
+ try:
+ response = get_gerrit_plugin_list(
+ f"http://{gerrit_deployment.hostname}",
+ "gerrit-admin",
+ ldap_credentials["gerrit-admin"],
+ )
+ if response is not None and removed_plugin["name"] not in response:
+ break
+ except requests.exceptions.ConnectionError:
+ time.sleep(1)
+
+ assert removed_plugin["name"] not in response
+ self._assert_installed_plugins(installed_plugins, response)
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritChartLibModuleInstall:
+ def _assert_installed_libs(self, expected_libs, installed_libs):
+ for lib in expected_libs:
+ assert lib["name"] in installed_libs
+
+ @pytest.mark.timeout(300)
+ def test_install_libs(self, gerrit_deployment_with_libs):
+ gerrit_deployment, expected_libs = gerrit_deployment_with_libs
+ response = get_gerrit_lib_list(gerrit_deployment)
+ self._assert_installed_libs(expected_libs, response)
+
+ @pytest.mark.timeout(300)
+ def test_install_other_plugins_are_removed_with_update(
+ self, gerrit_deployment_with_libs
+ ):
+ gerrit_deployment, installed_libs = gerrit_deployment_with_libs
+ removed_lib = installed_libs.pop()
+ gerrit_deployment.set_helm_value("gerrit.pluginManagement.libs", installed_libs)
+ gerrit_deployment.update()
+
+ response = None
+ while True:
+ try:
+ response = get_gerrit_lib_list(gerrit_deployment)
+ if response is not None and removed_lib["name"] not in response:
+ break
+ except requests.exceptions.ConnectionError:
+ time.sleep(1)
+
+ assert removed_lib["name"] not in response
+ self._assert_installed_libs(installed_libs, response)
+
+
+@pytest.mark.integration
+@pytest.mark.kubernetes
+@pytest.mark.timeout(180)
+def test_install_other_plugins_fails_wrong_sha(
+ gerrit_deployment_with_other_plugin_wrong_sha,
+):
+ pod_labels = f"app.kubernetes.io/component=gerrit,release={gerrit_deployment_with_other_plugin_wrong_sha.chart_name}"
+ core_v1 = client.CoreV1Api()
+ pod_name = ""
+ while not pod_name:
+ pod_list = core_v1.list_namespaced_pod(
+ namespace=gerrit_deployment_with_other_plugin_wrong_sha.namespace,
+ watch=False,
+ label_selector=pod_labels,
+ )
+ if len(pod_list.items) > 1:
+ raise RuntimeError("Too many gerrit pods with the same release name.")
+ elif len(pod_list.items) == 1:
+ pod_name = pod_list.items[0].metadata.name
+
+ current_status = None
+ while not current_status:
+ pod = core_v1.read_namespaced_pod_status(
+ pod_name, gerrit_deployment_with_other_plugin_wrong_sha.namespace
+ )
+ if not pod.status.init_container_statuses:
+ time.sleep(1)
+ continue
+ for init_container_status in pod.status.init_container_statuses:
+ if (
+ init_container_status.name == "gerrit-init"
+ and init_container_status.last_state.terminated
+ ):
+ current_status = init_container_status
+ assert current_status.last_state.terminated.exit_code > 0
+ return
+
+ assert current_status.last_state.terminated.exit_code > 0
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_setup.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_setup.py
new file mode 100644
index 0000000..306d41c
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_setup.py
@@ -0,0 +1,29 @@
+# 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 pytest
+
+
+@pytest.mark.integration
+@pytest.mark.kubernetes
+def test_deployment(test_cluster, default_gerrit_deployment):
+ installed_charts = test_cluster.helm.list(default_gerrit_deployment.namespace)
+ gerrit_chart = None
+ for chart in installed_charts:
+ if chart["name"].startswith("gerrit"):
+ gerrit_chart = chart
+ assert gerrit_chart is not None
+ assert gerrit_chart["status"].lower() == "deployed"
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_smoke_test.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_smoke_test.py
new file mode 100644
index 0000000..b3ee757
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_smoke_test.py
@@ -0,0 +1,110 @@
+# 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 re
+import shutil
+
+from pathlib import Path
+
+import pygit2 as git
+import pytest
+import requests
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def admin_creds(request):
+ user = request.config.getoption("--gerrit-user")
+ pwd = request.config.getoption("--gerrit-pwd")
+ return user, pwd
+
+
+@pytest.fixture(scope="class")
+def tmp_test_repo(request, tmp_path_factory):
+ tmp_dir = tmp_path_factory.mktemp("gerrit_chart_clone_test")
+ yield tmp_dir
+ shutil.rmtree(tmp_dir)
+
+
+@pytest.fixture(scope="class")
+def random_repo_name():
+ return utils.create_random_string(16)
+
+
+@pytest.mark.smoke
+def test_ui_connection(request):
+ response = requests.get(request.config.getoption("--ingress-url"))
+ assert response.status_code == requests.codes["OK"]
+ assert re.search(r'content="Gerrit Code Review"', response.text)
+
+
+@pytest.mark.smoke
+@pytest.mark.incremental
+class TestGerritRestGitCalls:
+ def _is_delete_project_plugin_enabled(self, gerrit_url, user, pwd):
+ url = f"{gerrit_url}/a/plugins/delete-project/gerrit~status"
+ response = requests.get(url, auth=(user, pwd))
+ return response.status_code == requests.codes["OK"]
+
+ def test_create_project_rest(self, request, random_repo_name, admin_creds):
+ ingress_url = request.config.getoption("--ingress-url")
+ create_project_url = f"{ingress_url}/a/projects/{random_repo_name}"
+ response = requests.put(create_project_url, auth=admin_creds)
+ assert response.status_code == requests.codes["CREATED"]
+
+ def test_cloning_project(
+ self, request, tmp_test_repo, random_repo_name, admin_creds
+ ):
+ repo_url = f"{request.config.getoption('--ingress-url')}/{random_repo_name}.git"
+ repo_url = repo_url.replace("//", f"//{admin_creds[0]}:{admin_creds[1]}@")
+ repo = git.clone_repository(repo_url, tmp_test_repo)
+ assert repo.path == os.path.join(tmp_test_repo, ".git/")
+
+ def test_push_commit(self, tmp_test_repo):
+ repo = git.Repository(tmp_test_repo)
+ file_name = os.path.join(tmp_test_repo, "test.txt")
+ Path(file_name).touch()
+ repo.index.add("test.txt")
+ repo.index.write()
+ # pylint: disable=E1101
+ author = git.Signature("Gerrit Review", "gerrit@review.com")
+ committer = git.Signature("Gerrit Review", "gerrit@review.com")
+ message = "Initial commit"
+ tree = repo.index.write_tree()
+ repo.create_commit("HEAD", author, committer, message, tree, [])
+
+ origin = repo.remotes["origin"]
+ origin.push(["refs/heads/master:refs/heads/master"])
+
+ remote_refs = origin.ls_remotes()
+ assert remote_refs[0]["name"] == repo.revparse_single("HEAD").hex
+
+ def test_delete_project_rest(self, request, random_repo_name, admin_creds):
+ ingress_url = request.config.getoption("--ingress-url")
+ if not self._is_delete_project_plugin_enabled(
+ ingress_url, admin_creds[0], admin_creds[1]
+ ):
+ pytest.skip(
+ "Delete-project plugin not installed."
+ + f"The test project ({random_repo_name}) has to be deleted manually."
+ )
+ project_url = (
+ f"{ingress_url}/a/projects/{random_repo_name}/delete-project~delete"
+ )
+ response = requests.post(project_url, auth=admin_creds)
+ assert response.status_code == requests.codes["NO_CONTENT"]
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_ssl.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_ssl.py
new file mode 100644
index 0000000..0eee0f4
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_ssl.py
@@ -0,0 +1,89 @@
+# 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 pygit2 as git
+import pytest
+import requests
+
+import git_callbacks
+import mock_ssl
+
+
+@pytest.fixture(scope="module")
+def cert_dir(tmp_path_factory):
+ return tmp_path_factory.mktemp("gerrit-cert")
+
+
+def _create_ssl_certificate(url, cert_dir):
+ keypair = mock_ssl.MockSSLKeyPair("*." + url.split(".", 1)[1], url)
+ with open(os.path.join(cert_dir, "server.crt"), "wb") as f:
+ f.write(keypair.get_cert())
+ with open(os.path.join(cert_dir, "server.key"), "wb") as f:
+ f.write(keypair.get_key())
+ return keypair
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment_with_ssl(cert_dir, gerrit_deployment):
+ ssl_certificate = _create_ssl_certificate(gerrit_deployment.hostname, cert_dir)
+ gerrit_deployment.set_helm_value("ingress.tls.enabled", True)
+ gerrit_deployment.set_helm_value(
+ "ingress.tls.cert", ssl_certificate.get_cert().decode()
+ )
+ gerrit_deployment.set_helm_value(
+ "ingress.tls.key", ssl_certificate.get_key().decode()
+ )
+ gerrit_deployment.set_gerrit_config_value(
+ "httpd", "listenUrl", "proxy-https://*:8080/"
+ )
+ gerrit_deployment.set_gerrit_config_value(
+ "gerrit",
+ "canonicalWebUrl",
+ f"https://{gerrit_deployment.hostname}",
+ )
+
+ gerrit_deployment.install()
+ gerrit_deployment.create_admin_account()
+
+ yield gerrit_deployment
+
+
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+@pytest.mark.slow
+class TestgerritChartSetup:
+ # pylint: disable=W0613
+ def test_create_project_rest(
+ self, cert_dir, gerrit_deployment_with_ssl, ldap_credentials
+ ):
+ create_project_url = (
+ f"https://{gerrit_deployment_with_ssl.hostname}/a/projects/test"
+ )
+ response = requests.put(
+ create_project_url,
+ auth=("gerrit-admin", ldap_credentials["gerrit-admin"]),
+ verify=os.path.join(cert_dir, "server.crt"),
+ )
+ assert response.status_code == 201
+
+ def test_cloning_project(self, tmp_path_factory, gerrit_deployment_with_ssl):
+ clone_dest = tmp_path_factory.mktemp("gerrit_chart_clone_test")
+ repo_url = f"https://{gerrit_deployment_with_ssl.hostname}/test.git"
+ repo = git.clone_repository(
+ repo_url, clone_dest, callbacks=git_callbacks.TestRemoteCallbacks()
+ )
+ assert repo.path == os.path.join(clone_dest, ".git/")
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_usage.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_usage.py
new file mode 100644
index 0000000..f63d209
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_usage.py
@@ -0,0 +1,51 @@
+# 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 pygit2 as git
+import pytest
+import requests
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritChartSetup:
+ @pytest.mark.timeout(240)
+ def test_create_project_rest(self, default_gerrit_deployment, ldap_credentials):
+ create_project_url = (
+ f"http://{default_gerrit_deployment.hostname}/a/projects/test"
+ )
+ response = None
+
+ while not response:
+ try:
+ response = requests.put(
+ create_project_url,
+ auth=("gerrit-admin", ldap_credentials["gerrit-admin"]),
+ )
+ except requests.exceptions.ConnectionError:
+ break
+
+ assert response.status_code == 201
+
+ def test_cloning_project(self, tmp_path_factory, default_gerrit_deployment):
+ clone_dest = tmp_path_factory.mktemp("gerrit_chart_clone_test")
+ repo_url = f"http://{default_gerrit_deployment.hostname}/test.git"
+ repo = git.clone_repository(repo_url, clone_dest)
+ assert repo.path == os.path.join(clone_dest, ".git/")
diff --git a/charts/k8s-gerrit/tests/helpers/git_callbacks.py b/charts/k8s-gerrit/tests/helpers/git_callbacks.py
new file mode 100644
index 0000000..3922a24
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helpers/git_callbacks.py
@@ -0,0 +1,20 @@
+# 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 pygit2 as git
+
+
+class TestRemoteCallbacks(git.RemoteCallbacks):
+ def certificate_check(self, certificate, valid, host):
+ return True
diff --git a/charts/k8s-gerrit/tests/helpers/mock_ssl.py b/charts/k8s-gerrit/tests/helpers/mock_ssl.py
new file mode 100644
index 0000000..46d766c
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helpers/mock_ssl.py
@@ -0,0 +1,49 @@
+# 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.
+
+from OpenSSL import crypto
+
+
+class MockSSLKeyPair:
+ def __init__(self, common_name, subject_alt_name):
+ self.common_name = common_name
+ self.subject_alt_name = subject_alt_name
+ self.cert = None
+ self.key = None
+
+ self._create_keypair()
+
+ def _create_keypair(self):
+ self.key = crypto.PKey()
+ self.key.generate_key(crypto.TYPE_RSA, 2048)
+
+ self.cert = crypto.X509()
+ self.cert.set_version(2)
+ self.cert.get_subject().O = "Gerrit"
+ self.cert.get_subject().CN = self.common_name
+ san = f"DNS:{self.subject_alt_name}"
+ self.cert.add_extensions(
+ [crypto.X509Extension(b"subjectAltName", False, san.encode())]
+ )
+ self.cert.gmtime_adj_notBefore(0)
+ self.cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
+ self.cert.set_issuer(self.cert.get_subject())
+ self.cert.set_pubkey(self.key)
+ self.cert.sign(self.key, "sha256")
+
+ def get_key(self):
+ return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key)
+
+ def get_cert(self):
+ return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
diff --git a/charts/k8s-gerrit/tests/helpers/utils.py b/charts/k8s-gerrit/tests/helpers/utils.py
new file mode 100644
index 0000000..804217e
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helpers/utils.py
@@ -0,0 +1,41 @@
+# 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 random
+import string
+
+
+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
+
+ Arguments:
+ image {docker.images.Image} -- Docker image object to be checked
+ ancestor {str} -- Complete name of the expected ancestor image
+
+ Returns:
+ boolean -- True, if ancestor is inherited by image
+ """
+
+ contains_tag = False
+ for layer in image.history():
+ contains_tag = layer["Tags"] is not None and ancestor in layer["Tags"]
+ 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()