Allow to install custom Gerrit-plugins in gerrit-master chart
It was not yet possible to install plugins that were not packaged plugins in
the gerrit-master chart.
This change allows to configure a list of plugins to install for Gerrit.
To do this, it is required to give a URL, where the plugin jar can be
downloaded. Additionally, a SHA1-checksum has to be provided to ensure that
the same jar is used for each pod. The plugins are downloaded in the
`gerrit-init` init-container before Gerrit starts up, as long as the jar
is not yet present in the plugins directory. Optionally, the plugin
files can be cached.
Change-Id: I34e6bc5668090b37c9749d4df72eeb719fdf3807
diff --git a/container-images/gerrit-init/README.md b/container-images/gerrit-init/README.md
index 5266dbb..4cf03f7 100644
--- a/container-images/gerrit-init/README.md
+++ b/container-images/gerrit-init/README.md
@@ -17,6 +17,14 @@
* start the container via start script `/var/tools/gerrit_init.py`
+The `download_plugins.py`-script
+
+* parses required plugins from config file
+* removes unwanted plugins
+* installs and updates plugins not packaged in Gerrit's war-file
+* plugin files are validated using SHA1
+* plugin files may optionally be cached
+
The `gerrit_init.py`-script
* reads configuration from gerrit.config (via `gerrit_config_parser.py`)
diff --git a/container-images/gerrit-init/config/default.config.yaml b/container-images/gerrit-init/config/default.config.yaml
index e69de29..65b7b28 100644
--- a/container-images/gerrit-init/config/default.config.yaml
+++ b/container-images/gerrit-init/config/default.config.yaml
@@ -0,0 +1 @@
+pluginCache: false
diff --git a/container-images/gerrit-init/dependencies/Pipfile b/container-images/gerrit-init/dependencies/Pipfile
index 32dcbb1..166e043 100644
--- a/container-images/gerrit-init/dependencies/Pipfile
+++ b/container-images/gerrit-init/dependencies/Pipfile
@@ -7,6 +7,7 @@
[packages]
pyyaml = "~=5.1.1"
+requests = "~=2.21.0"
[requires]
python_version = "3.7"
diff --git a/container-images/gerrit-init/dependencies/Pipfile.lock b/container-images/gerrit-init/dependencies/Pipfile.lock
index 4196274..8c818e8 100644
--- a/container-images/gerrit-init/dependencies/Pipfile.lock
+++ b/container-images/gerrit-init/dependencies/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "4c5889e94a1b967f4cf238d7c5265e62e03611deb65b66c4a77f1673505436d7"
+ "sha256": "f8c5bd58b89445bd252681282fb1144e20c6b22274e58b2a5221af137273c832"
},
"pipfile-spec": 6,
"requires": {
@@ -16,6 +16,27 @@
]
},
"default": {
+ "certifi": {
+ "hashes": [
+ "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
+ "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
+ ],
+ "version": "==2019.6.16"
+ },
+ "chardet": {
+ "hashes": [
+ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
+ "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+ ],
+ "version": "==3.0.4"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
+ "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+ ],
+ "version": "==2.8"
+ },
"pyyaml": {
"hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
@@ -34,6 +55,21 @@
],
"index": "pypi",
"version": "==5.1.2"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
+ "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
+ ],
+ "index": "pypi",
+ "version": "==2.21.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4",
+ "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb"
+ ],
+ "version": "==1.24.3"
}
},
"develop": {}
diff --git a/container-images/gerrit-init/tools/download_plugins.py b/container-images/gerrit-init/tools/download_plugins.py
new file mode 100755
index 0000000..3615b93
--- /dev/null
+++ b/container-images/gerrit-init/tools/download_plugins.py
@@ -0,0 +1,234 @@
+#!/usr/bin/python3
+
+# 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 argparse
+import hashlib
+import os
+import time
+
+from abc import ABC, abstractmethod
+
+import requests
+
+from init_config import InitConfig
+from log import get_logger
+
+LOG = get_logger("init")
+MAX_LOCK_LIFETIME = 60
+MAX_CACHED_VERSIONS = 5
+
+
+class InvalidPluginException(Exception):
+ """ Exception to be raised, if the downloaded plugin is not valid. """
+
+
+class AbstractPluginInstaller(ABC):
+ def __init__(self, site, config):
+ self.site = site
+ self.config = config
+
+ self.plugin_dir = os.path.join(site, "plugins")
+ self.plugins_changed = False
+
+ def _create_plugins_dir(self):
+ if not os.path.exists(self.plugin_dir):
+ os.makedirs(self.plugin_dir)
+ LOG.info("Created plugin installation directory: %s", self.plugin_dir)
+
+ def _get_installed_plugins(self):
+ if os.path.exists(self.plugin_dir):
+ return [f for f in os.listdir(self.plugin_dir) if f.endswith(".jar")]
+
+ return list()
+
+ @staticmethod
+ def _get_file_sha(file):
+ file_hash = hashlib.sha1()
+ with open(file, "rb") as f:
+ while True:
+ chunk = f.read(64000)
+ if not chunk:
+ break
+ file_hash.update(chunk)
+
+ LOG.debug("SHA1 of file '%s' is %s", file, file_hash.hexdigest())
+
+ return file_hash.hexdigest()
+
+ def _remove_unwanted_plugins(self):
+ wanted_plugins = [plugin["name"] for plugin in self.config.downloaded_plugins]
+ wanted_plugins.extend(self.config.packaged_plugins)
+ for plugin in self._get_installed_plugins():
+ if os.path.splitext(plugin)[0] not in wanted_plugins:
+ os.remove(os.path.join(self.plugin_dir, plugin))
+ LOG.info("Removed plugin %s", plugin)
+
+ def execute(self):
+ self._create_plugins_dir()
+ self._remove_unwanted_plugins()
+
+ for plugin in self.config.downloaded_plugins:
+ self._install_plugin(plugin)
+
+ def _download_plugin(self, plugin, target):
+ LOG.info("Downloading %s plugin to %s", plugin["name"], target)
+ response = requests.get(plugin["url"])
+ with open(target, "wb") as f:
+ f.write(response.content)
+
+ file_sha = self._get_file_sha(target)
+
+ if file_sha != plugin["sha1"]:
+ os.remove(target)
+ raise InvalidPluginException(
+ (
+ "SHA1 of downloaded file (%s) did not match expected SHA1 (%s). "
+ "Removed downloaded file (%s)"
+ )
+ % (file_sha, plugin["sha1"], target)
+ )
+
+ @abstractmethod
+ def _install_plugin(self, plugin):
+ pass
+
+
+class PluginInstaller(AbstractPluginInstaller):
+ def _install_plugin(self, plugin):
+ target = os.path.join(self.plugin_dir, "%s.jar" % plugin["name"])
+ if os.path.exists(target) and self._get_file_sha(target) == plugin["sha1"]:
+ return
+
+ self._download_plugin(plugin, target)
+
+ self.plugins_changed = True
+
+
+class CachedPluginInstaller(AbstractPluginInstaller):
+ @staticmethod
+ def _cleanup_cache(plugin_cache_dir):
+ cached_files = [
+ os.path.join(plugin_cache_dir, f) for f in os.listdir(plugin_cache_dir)
+ ]
+ while len(cached_files) > MAX_CACHED_VERSIONS:
+ oldest_file = min(cached_files, key=os.path.getctime)
+ LOG.info(
+ "Too many cached files in %s. Removing file %s",
+ plugin_cache_dir,
+ oldest_file,
+ )
+ os.remove(oldest_file)
+ cached_files.remove(oldest_file)
+
+ @staticmethod
+ def _create_download_lock(lock_path):
+ with open(lock_path, "w") as f:
+ f.write(os.environ["HOSTNAME"])
+ LOG.debug("Created download lock %s", lock_path)
+
+ @staticmethod
+ def _create_plugin_cache_dir(plugin_cache_dir):
+ if not os.path.exists(plugin_cache_dir):
+ os.makedirs(plugin_cache_dir)
+ LOG.info("Created cache directory %s", plugin_cache_dir)
+
+ def _get_cached_plugin_path(self, plugin):
+ return os.path.join(
+ self.config.plugin_cache_dir,
+ plugin["name"],
+ "%s-%s.jar" % (plugin["name"], plugin["sha1"]),
+ )
+
+ def _install_from_cache_or_download(self, plugin, target):
+ cached_plugin_path = self._get_cached_plugin_path(plugin)
+
+ if os.path.exists(cached_plugin_path):
+ LOG.info("Installing %s plugin from cache.", plugin["name"])
+ else:
+ LOG.info("%s not found in cache. Downloading it.", plugin["name"])
+ download_target = self._get_cached_plugin_path(plugin)
+ self._create_plugin_cache_dir(os.path.dirname(target))
+
+ lock_path = "%s.lock" % download_target
+ while os.path.exists(lock_path):
+ LOG.info(
+ "Download lock found (%s). Waiting %d seconds for it to be released.",
+ lock_path,
+ MAX_LOCK_LIFETIME,
+ )
+ lock_timestamp = os.path.getmtime(lock_path)
+ if time.time() > lock_timestamp + MAX_LOCK_LIFETIME:
+ LOG.info("Stale download lock found (%s).", lock_path)
+ self._remove_download_lock(lock_path)
+
+ self._create_download_lock(lock_path)
+
+ try:
+ self._download_plugin(plugin, download_target)
+ finally:
+ self._remove_download_lock(lock_path)
+
+ os.symlink(cached_plugin_path, target)
+ self._cleanup_cache(os.path.dirname(target))
+
+ def _install_plugin(self, plugin):
+ install_path = os.path.join(self.plugin_dir, "%s.jar" % plugin["name"])
+ if (
+ os.path.exists(install_path)
+ and self._get_file_sha(install_path) == plugin["sha1"]
+ ):
+ return
+
+ self.plugins_changed = True
+ self._install_from_cache_or_download(plugin, install_path)
+
+ @staticmethod
+ def _remove_download_lock(lock_path):
+ os.remove(lock_path)
+ LOG.debug("Removed download lock %s", lock_path)
+
+
+def get_installer(site, config):
+ plugin_installer = (
+ CachedPluginInstaller if config.plugin_cache_enabled else PluginInstaller
+ )
+ return plugin_installer(site, config)
+
+
+# pylint: disable=C0103
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-s",
+ "--site",
+ help="Path to Gerrit site",
+ dest="site",
+ action="store",
+ default="/var/gerrit",
+ required=True,
+ )
+ parser.add_argument(
+ "-c",
+ "--config",
+ help="Path to configuration file for init process.",
+ dest="config",
+ action="store",
+ required=True,
+ )
+ args = parser.parse_args()
+
+ config = InitConfig().parse(args.config)
+ get_installer(args.site, config).execute()
diff --git a/container-images/gerrit-init/tools/gerrit_init.py b/container-images/gerrit-init/tools/gerrit_init.py
index f4be415..bef04c8 100755
--- a/container-images/gerrit-init/tools/gerrit_init.py
+++ b/container-images/gerrit-init/tools/gerrit_init.py
@@ -19,6 +19,7 @@
import subprocess
import sys
+from download_plugins import get_installer
from git_config_parser import GitConfigParser
from init_config import InitConfig
from log import get_logger
@@ -31,6 +32,8 @@
self.site = site
self.config = config
+ self.plugin_installer = get_installer(self.site, self.config)
+
self.gerrit_config = self._parse_gerrit_config()
self.is_slave = self._is_slave()
self.installed_plugins = self._get_installed_plugins()
@@ -65,11 +68,6 @@
return installed_plugins
- def _remove_unwanted_plugins(self):
- for plugin in self.installed_plugins.difference(self.config.packaged_plugins):
- LOG.info("Removing plugin %s", plugin)
- os.remove(os.path.join(self.site, "plugins", "%s.jar" % plugin))
-
def _gerrit_war_updated(self):
installed_war_path = os.path.join(self.site, "bin", "gerrit.war")
installed_version = self._get_gerrit_version(installed_war_path)
@@ -82,6 +80,10 @@
return installed_version != provided_version
def _needs_init(self):
+ if self.plugin_installer.plugins_changed:
+ LOG.info("Plugins were installed or updated. Initializing.")
+ return True
+
installed_war_path = os.path.join(self.site, "bin", "gerrit.war")
if not os.path.exists(installed_war_path):
LOG.info("Gerrit is not yet installed. Initializing new site.")
@@ -99,7 +101,7 @@
return False
def execute(self):
- self._remove_unwanted_plugins()
+ self.plugin_installer.execute()
if not self._needs_init():
return
diff --git a/container-images/gerrit-init/tools/init_config.py b/container-images/gerrit-init/tools/init_config.py
index 31d9394..266fdde 100644
--- a/container-images/gerrit-init/tools/init_config.py
+++ b/container-images/gerrit-init/tools/init_config.py
@@ -19,7 +19,10 @@
class InitConfig:
def __init__(self):
+ self.downloaded_plugins = list()
+ self.plugin_cache_enabled = False
self.packaged_plugins = set()
+ self.plugin_cache_dir = None
def parse(self, config_file):
if not os.path.exists(config_file):
@@ -31,7 +34,13 @@
if config is None:
raise ValueError("Invalid config-file: %s" % config_file)
+ if "downloadedPlugins" in config:
+ self.downloaded_plugins = config["downloadedPlugins"]
if "packagedPlugins" in config:
self.packaged_plugins = set(config["packagedPlugins"])
+ if "pluginCache" in config:
+ self.plugin_cache_enabled = config["pluginCache"]
+ if "pluginCacheDir" in config and config["pluginCacheDir"]:
+ self.plugin_cache_dir = config["pluginCacheDir"]
return self
diff --git a/helm-charts/gerrit-master/README.md b/helm-charts/gerrit-master/README.md
index 702d17f..be13361 100644
--- a/helm-charts/gerrit-master/README.md
+++ b/helm-charts/gerrit-master/README.md
@@ -165,6 +165,12 @@
| `gerritMaster.ingress.tls.key` | Private SSL server certificate | `-----BEGIN RSA PRIVATE KEY-----` |
| `gerritMaster.keystore` | base64-encoded Java keystore (`cat keystore.jks | base64`) to be used by Gerrit, when using SSL | `nil` |
| `gerritMaster.plugins.packaged` | List of Gerrit plugins that are packaged into the Gerrit-war-file to install | `["commit-message-length-validator", "download-commands", "replication", "reviewnotes"]` |
+| `gerritMaster.plugins.downloaded` | List of Gerrit plugins that will be downloaded | `nil` |
+| `gerritMaster.plugins.downloaded[0].name` | Name of plugin | `nil` |
+| `gerritMaster.plugins.downloaded[0].url` | Download url of plugin | `nil` |
+| `gerritMaster.plugins.downloaded[0].sha1` | SHA1 sum of plugin jar used to ensure file integrity and version (optional) | `nil` |
+| `gerritMaster.plugins.cache.enabled` | Whether to cache downloaded plugins | `false` |
+| `gerritMaster.plugins.cache.size` | Size of the volume used to store cached plugins | `1Gi` |
| `gerritMaster.config.gerrit` | The contents of the gerrit.config | [see here](#Gerrit-config-files) |
| `gerritMaster.config.secure` | The contents of the secure.config | [see here](#Gerrit-config-files) |
| `gerritMaster.config.replication` | The contents of the replication.config | [see here](#Gerrit-config-files) |
@@ -220,6 +226,24 @@
memory resource limit set for the container (e.g. `-Xmx4g`). In your calculation,
allow memory for other components running in the container.
+### Installing Gerrit plugins
+
+There are several different ways to install plugins for Gerrit:
+
+- **RECOMMENDED: Package the plugins to install into the WAR-file containing Gerrit.**
+ This method provides the most stable way to install plugins, but requires to
+ use a custom built gerrit-war file and container images, if plugins are required
+ that are not part of the official `release.war`-file.
+
+- **Download and cache plugins.** The chart supports downloading the plugin files and
+ to cache them in a separate volume, that is shared between Gerrit-pods. SHA1-
+ sums are used to validate plugin-files and versions.
+
+- **Download plugins, but do not cache them.** This should only be used during
+ development to save resources (the shared volume). Each pod will download the
+ plugin-files on its own. Pods will fail to start up, if the download-URL is
+ not valid anymore at some point in time.
+
## Upgrading the Chart
To upgrade an existing installation of the gerrit-master chart, e.g. to install
diff --git a/helm-charts/gerrit-master/templates/gerrit-master.configmap.yaml b/helm-charts/gerrit-master/templates/gerrit-master.configmap.yaml
index 30cddc6..814fb68 100644
--- a/helm-charts/gerrit-master/templates/gerrit-master.configmap.yaml
+++ b/helm-charts/gerrit-master/templates/gerrit-master.configmap.yaml
@@ -24,7 +24,13 @@
release: {{ .Release.Name }}
data:
gerrit-init.yaml: |-
+ pluginCache: {{ .Values.gerritMaster.plugins.cache.enabled }}
+ pluginCacheDir: /var/mnt/plugins
{{- if .Values.gerritMaster.plugins.packaged }}
packagedPlugins:
{{ toYaml .Values.gerritMaster.plugins.packaged | indent 6}}
{{- end }}
+ {{- if .Values.gerritMaster.plugins.downloaded }}
+ downloadedPlugins:
+{{ toYaml .Values.gerritMaster.plugins.downloaded | indent 6 }}
+ {{- end }}
diff --git a/helm-charts/gerrit-master/templates/gerrit-master.stateful-set.yaml b/helm-charts/gerrit-master/templates/gerrit-master.stateful-set.yaml
index dd8575f..9448ddd 100644
--- a/helm-charts/gerrit-master/templates/gerrit-master.stateful-set.yaml
+++ b/helm-charts/gerrit-master/templates/gerrit-master.stateful-set.yaml
@@ -84,6 +84,10 @@
- name: gerrit-init-config
mountPath: "/var/config/gerrit-init.yaml"
subPath: gerrit-init.yaml
+ {{- if and .Values.gerritMaster.plugins.cache.enabled .Values.gerritMaster.plugins.downloaded }}
+ - name: gerrit-plugin-cache
+ mountPath: "/var/mnt/plugins"
+ {{- end }}
- name: gerrit-config
mountPath: "/var/config/gerrit.config"
subPath: gerrit.config
@@ -138,6 +142,11 @@
- name: gerrit-site
emptyDir: {}
{{- end }}
+ {{- if and .Values.gerritMaster.plugins.cache.enabled .Values.gerritMaster.plugins.downloaded }}
+ - name: gerrit-plugin-cache
+ persistentVolumeClaim:
+ claimName: {{ .Release.Name }}-plugin-cache-pvc
+ {{- end }}
- name: git-filesystem
persistentVolumeClaim:
{{- if .Values.gitRepositoryStorage.externalPVC.use }}
diff --git a/helm-charts/gerrit-master/templates/gerrit-master.storage.yaml b/helm-charts/gerrit-master/templates/gerrit-master.storage.yaml
new file mode 100644
index 0000000..300afa6
--- /dev/null
+++ b/helm-charts/gerrit-master/templates/gerrit-master.storage.yaml
@@ -0,0 +1,18 @@
+{{- if and .Values.gerritMaster.plugins.cache.enabled .Values.gerritMaster.plugins.downloaded }}
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+ name: {{ .Release.Name }}-plugin-cache-pvc
+ labels:
+ app: gerrit-master
+ chart: {{ template "gerrit-master.chart" . }}
+ heritage: {{ .Release.Service }}
+ release: {{ .Release.Name }}
+spec:
+ accessModes:
+ - ReadWriteMany
+ resources:
+ requests:
+ storage: {{ .Values.gerritMaster.plugins.cache.size }}
+ storageClassName: {{ .Values.storageClasses.shared.name }}
+{{- end }}
diff --git a/helm-charts/gerrit-master/values.yaml b/helm-charts/gerrit-master/values.yaml
index ccdabf9..67aef6c 100644
--- a/helm-charts/gerrit-master/values.yaml
+++ b/helm-charts/gerrit-master/values.yaml
@@ -132,6 +132,16 @@
- download-commands
- replication
- reviewnotes
+ downloaded:
+ # - name: delete-project
+ # url: https://example.com/gerrit-plugins/delete-project.jar
+ # sha1:
+
+ # Only downloaded plugins will be cached. This will be ignored, if no plugins
+ # are downloaded.
+ cache:
+ enabled: false
+ size: 1Gi
config:
# Some values are expected to have a specific value for the deployment installed
diff --git a/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py b/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py
index 38d37ec..ab94fc8 100755
--- a/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py
+++ b/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py
@@ -27,6 +27,7 @@
@pytest.fixture(
scope="function",
params=[
+ "/var/tools/download_plugins.py",
"/var/tools/gerrit_init.py",
"/var/tools/git_config_parser.py",
"/var/tools/init_config.py",
@@ -41,7 +42,7 @@
return request.param
-@pytest.fixture(scope="function", params=["pyyaml"])
+@pytest.fixture(scope="function", params=["pyyaml", "requests"])
def expected_pip_package(request):
return request.param
diff --git a/tests/helm-charts/gerrit-master/conftest.py b/tests/helm-charts/gerrit-master/conftest.py
index 1331fc3..99cb187 100644
--- a/tests/helm-charts/gerrit-master/conftest.py
+++ b/tests/helm-charts/gerrit-master/conftest.py
@@ -35,7 +35,7 @@
gitgc_image,
gerrit_init_image,
):
- def install_chart(chart_opts):
+ def install_chart(chart_opts, wait=True):
chart_path = os.path.join(repository_root, "helm-charts", "gerrit-master")
chart_name = "gerrit-master-" + utils.create_random_string()
namespace_name = utils.create_random_string()
@@ -67,6 +67,7 @@
set_values=chart_opts,
fail_on_err=True,
namespace=namespace_name,
+ wait=wait,
)
return {"chart": chart_path, "name": chart_name, "namespace": namespace_name}
diff --git a/tests/helm-charts/gerrit-master/test_chart_gerrit_master_plugins.py b/tests/helm-charts/gerrit-master/test_chart_gerrit_master_plugins.py
index d4470ec..2426da3 100644
--- a/tests/helm-charts/gerrit-master/test_chart_gerrit_master_plugins.py
+++ b/tests/helm-charts/gerrit-master/test_chart_gerrit_master_plugins.py
@@ -14,14 +14,36 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import hashlib
import json
import time
import pytest
import requests
+from kubernetes import client
+
import utils
+PLUGINS = ["owners", "reviewers"]
+GERRIT_VERSION = "3.0"
+
+
+@pytest.fixture(scope="module")
+def plugin_list():
+ plugin_list = list()
+ for plugin in PLUGINS:
+ url = (
+ "https://gerrit-ci.gerritforge.com/view/Plugins-stable-{gerrit_version}/"
+ "job/plugin-{plugin}-bazel-stable-{gerrit_version}/lastSuccessfulBuild/"
+ "artifact/bazel-bin/plugins/{plugin}/{plugin}.jar"
+ ).format(plugin=plugin, gerrit_version=GERRIT_VERSION)
+ 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="class")
def gerrit_master_deployment_with_plugins_factory(
@@ -66,6 +88,63 @@
test_cluster.delete_namespace(chart["namespace"])
+@pytest.fixture(
+ scope="class", params=[1, 2], ids=["single-other-plugin", "multiple-other-plugins"]
+)
+def gerrit_master_deployment_with_other_plugins(
+ request,
+ docker_tag,
+ test_cluster,
+ plugin_list,
+ gerrit_master_deployment_with_plugins_factory,
+):
+ chart_opts = {
+ "images.registry.name": request.config.getoption("--registry"),
+ "images.version": docker_tag,
+ "images.ImagePullPolicy": "IfNotPresent",
+ "gerritMaster.ingress.host": "master.%s"
+ % request.config.getoption("--ingress-url"),
+ }
+ selected_plugins = plugin_list[: request.param]
+ for counter, plugin in enumerate(selected_plugins):
+ chart_opts["gerritMaster.plugins.downloaded[%d].name" % counter] = plugin[
+ "name"
+ ]
+ chart_opts["gerritMaster.plugins.downloaded[%d].url" % counter] = plugin["url"]
+ chart_opts["gerritMaster.plugins.downloaded[%d].sha1" % counter] = plugin[
+ "sha1"
+ ]
+ chart = gerrit_master_deployment_with_plugins_factory(chart_opts)
+ chart["installed_plugins"] = selected_plugins
+
+ yield chart
+
+ test_cluster.helm.delete(chart["name"])
+
+
+@pytest.fixture()
+def gerrit_master_deployment_with_other_plugin_wrong_sha(
+ request, docker_tag, test_cluster, plugin_list, gerrit_master_deployment_factory
+):
+ chart_opts = {
+ "images.registry.name": request.config.getoption("--registry"),
+ "images.version": docker_tag,
+ "images.ImagePullPolicy": "IfNotPresent",
+ "gerritMaster.ingress.host": "master.%s"
+ % request.config.getoption("--ingress-url"),
+ }
+ plugin = plugin_list[0]
+ chart_opts["gerritMaster.plugins.downloaded[0].name"] = plugin["name"]
+ chart_opts["gerritMaster.plugins.downloaded[0].url"] = plugin["url"]
+ chart_opts["gerritMaster.plugins.downloaded[0].sha1"] = "notAValidSha"
+ chart = gerrit_master_deployment_factory(chart_opts, wait=False)
+ chart["installed_plugins"] = plugin
+
+ yield chart
+
+ test_cluster.helm.delete(chart["name"])
+
+
def update_chart(helm, chart, opts):
helm.upgrade(
chart=chart["chart"],
@@ -141,3 +220,121 @@
assert chart["removed_plugin"] not in response
self._assert_installed_plugins(chart["installed_plugins"], response)
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritMasterChartOtherPluginInstall:
+ def _remove_plugin_from_install_list(self, installed_plugins):
+ removed_plugin = installed_plugins.pop()
+ plugin_install_list = dict()
+ if installed_plugins:
+ for counter, plugin in enumerate(installed_plugins):
+ plugin_install_list[
+ "gerritMaster.plugins.downloaded[%d].name" % counter
+ ] = plugin["name"]
+ plugin_install_list[
+ "gerritMaster.plugins.downloaded[%d].url" % counter
+ ] = plugin["url"]
+ plugin_install_list[
+ "gerritMaster.plugins.downloaded[%d].sha1" % counter
+ ] = plugin["sha1"]
+ else:
+ plugin_install_list["gerritMaster.plugins.downloaded"] = "false"
+ return plugin_install_list, removed_plugin, installed_plugins
+
+ 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"]
+ == "%s.jar" % plugin["name"]
+ )
+
+ @pytest.mark.timeout(300)
+ def test_install_other_plugins(
+ self, request, test_cluster, gerrit_master_deployment_with_other_plugins
+ ):
+ response = None
+ while not response:
+ try:
+ response = get_gerrit_plugin_list(
+ "http://master.%s" % (request.config.getoption("--ingress-url"))
+ )
+ except requests.exceptions.ConnectionError:
+ continue
+ self._assert_installed_plugins(
+ gerrit_master_deployment_with_other_plugins["installed_plugins"], response
+ )
+
+ @pytest.mark.timeout(300)
+ def test_install_other_plugins_are_removed_with_update(
+ self, request, test_cluster, gerrit_master_deployment_with_other_plugins
+ ):
+ chart = gerrit_master_deployment_with_other_plugins
+ chart_opts, chart["removed_plugin"], chart[
+ "installed_plugin"
+ ] = self._remove_plugin_from_install_list(chart["installed_plugins"])
+ update_chart(test_cluster.helm, chart, chart_opts)
+
+ response = None
+ while True:
+ try:
+ response = get_gerrit_plugin_list(
+ "http://master.%s" % (request.config.getoption("--ingress-url"))
+ )
+ if (
+ response is not None
+ and chart["removed_plugin"]["name"] not in response
+ ):
+ break
+ except requests.exceptions.ConnectionError:
+ time.sleep(1)
+
+ assert chart["removed_plugin"]["name"] not in response
+ self._assert_installed_plugins(chart["installed_plugins"], response)
+
+
+@pytest.mark.integration
+@pytest.mark.kubernetes
+@pytest.mark.timeout(180)
+def test_install_other_plugins_fails_wrong_sha(
+ request, test_cluster, gerrit_master_deployment_with_other_plugin_wrong_sha
+):
+ pod_labels = "app=gerrit-master,release=%s" % (
+ gerrit_master_deployment_with_other_plugin_wrong_sha["name"]
+ )
+ core_v1 = client.CoreV1Api()
+ pod_name = ""
+ while not pod_name:
+ pod_list = core_v1.list_namespaced_pod(
+ namespace=gerrit_master_deployment_with_other_plugin_wrong_sha["namespace"],
+ watch=False,
+ label_selector=pod_labels,
+ )
+ if len(pod_list.items) > 1:
+ raise RuntimeError(
+ "Too many gerrit-master pods with the same release name."
+ )
+ 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_master_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/tests/helpers/helm.py b/tests/helpers/helm.py
index 1c5eb3f..18a0c2c 100644
--- a/tests/helpers/helm.py
+++ b/tests/helpers/helm.py
@@ -69,29 +69,31 @@
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
+ 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})
+ 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.
- """
+ Returns:
+ CompletedProcess -- CompletedProcess-object returned by subprocess
+ containing details about the result and output of the
+ executed command.
+ """
- helm_cmd = ["install", chart, "--dep-up", "-n", name, "--wait"]
+ helm_cmd = ["install", chart, "--dep-up", "-n", name]
if values_file:
helm_cmd.extend(("-f", values_file))
if set_values:
@@ -99,6 +101,8 @@
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):