update charts
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