update charts
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/.dockerignore b/charts/k8s-gerrit/container-images/gerrit-init/.dockerignore
new file mode 100644
index 0000000..7c68535
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/.dockerignore
@@ -0,0 +1 @@
+tools/__pycache__
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/Dockerfile b/charts/k8s-gerrit/container-images/gerrit-init/Dockerfile
new file mode 100644
index 0000000..70da7aa
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/Dockerfile
@@ -0,0 +1,29 @@
+ARG TAG=latest
+FROM gerrit-base:${TAG}
+
+USER root
+
+COPY dependencies/* /var/tools/
+COPY requirements.txt /var/tools/
+WORKDIR /var/tools
+
+RUN apk update && \
+ apk add --no-cache \
+ python3 && \
+ python3 -m ensurepip && \
+ rm -r /usr/lib/python*/ensurepip && \
+ # follow https://til.simonwillison.net/python/pip-tools to update hashes
+ pip3 install --require-hashes -r requirements.txt --no-cache --upgrade && \
+ pipenv install --python 3.11 --system
+
+COPY tools /var/tools/
+COPY config/* /var/config/
+
+RUN mkdir -p /var/mnt/git \
+ && mkdir -p /var/mnt/logs \
+ && chown -R gerrit:users /var/mnt
+
+USER gerrit
+
+ENTRYPOINT ["python3", "/var/tools/gerrit-initializer"]
+CMD ["-s", "/var/gerrit", "-c", "/var/config/gerrit-init.yaml", "init"]
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/README.md b/charts/k8s-gerrit/container-images/gerrit-init/README.md
new file mode 100644
index 0000000..37b5bda
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/README.md
@@ -0,0 +1,64 @@
+# Gerrit replica init container image
+
+Kubernetes init container for initializing gerrit. The python script running in
+the container initializes Gerrit including the installation of configured
+plugins.
+
+## Content
+
+* gerrit-base image
+
+## Setup and configuration
+
+* install python 3
+* copy tool scripts
+
+## Start
+
+* start the container via start script `python3 /var/tools/gerrit-initializer init`
+
+The `main.py init`-command
+
+* reads configuration from gerrit.config (via `gerrit_config_parser.py`)
+* initializes Gerrit
+
+The `main.py validate_notedb`-command
+
+* validates and waits for the repository `All-Projects.git` with the refs
+`refs/meta/config`.
+* validates and waits for the repository `All-Users.git` with the ref
+`refs/meta/config`.
+
+## Configuration
+
+The configuration format looks as follows:
+
+```yaml
+plugins: []
+# A plugin packaged in the gerrit.war-file
+# - name: download-commands
+
+# A plugin packaged in the gerrit.war-file that will also be installed as a
+# lib
+# - name: replication
+# installAsLibrary: true
+
+# A plugin that will be downloaded on startup
+# - name: delete-project
+# url: https://example.com/gerrit-plugins/delete-project.jar
+# sha1:
+# installAsLibrary: false
+libs: []
+# A lib that will be downloaded on startup
+# - name: global-refdb
+# url: https://example.com/gerrit-plugins/global-refdb.jar
+# sha1:
+#DEPRECATED: `pluginCache` was deprecated in favor of `pluginCacheEnabled`
+# pluginCache: true
+pluginCacheEnabled: false
+pluginCacheDir: null
+# Can be either true to use default CA certificates, false to disable SSL
+# verification or a path to a custom CA certificate store.
+caCertPath: true
+highAvailability: false
+```
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/config/gerrit-init.yaml b/charts/k8s-gerrit/container-images/gerrit-init/config/gerrit-init.yaml
new file mode 100644
index 0000000..65b7b28
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/config/gerrit-init.yaml
@@ -0,0 +1 @@
+pluginCache: false
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile b/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile
new file mode 100644
index 0000000..6a55c64
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile
@@ -0,0 +1,13 @@
+[[source]]
+name = "pypi"
+url = "https://pypi.org/simple"
+verify_ssl = true
+
+[dev-packages]
+
+[packages]
+pyyaml = "~=6.0"
+requests = "~=2.31.0"
+
+[requires]
+python_version = "3.11"
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile.lock b/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile.lock
new file mode 100644
index 0000000..10e814e
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile.lock
@@ -0,0 +1,180 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "bf7e62c1c2c8f726ef7dab0c66bddf079c2f4cee97a1d5a4d4546fcc4f41600f"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.11"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "certifi": {
+ "hashes": [
+ "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
+ "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==2023.5.7"
+ },
+ "charset-normalizer": {
+ "hashes": [
+ "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
+ "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
+ "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
+ "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
+ "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
+ "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
+ "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
+ "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
+ "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
+ "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
+ "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
+ "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
+ "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
+ "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
+ "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
+ "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
+ "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
+ "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
+ "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
+ "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
+ "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
+ "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
+ "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
+ "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
+ "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
+ "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
+ "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
+ "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
+ "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
+ "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
+ "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
+ "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
+ "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
+ "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
+ "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
+ "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
+ "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
+ "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
+ "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
+ "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
+ "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
+ "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
+ "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
+ "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
+ "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
+ "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
+ "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
+ "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
+ "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
+ "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
+ "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
+ "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
+ "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
+ "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
+ "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
+ "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
+ "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
+ "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
+ "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
+ "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
+ "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
+ "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
+ "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
+ "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
+ "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
+ "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
+ "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
+ "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
+ "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
+ "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
+ "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
+ "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
+ "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
+ "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
+ "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
+ ],
+ "markers": "python_full_version >= '3.7.0'",
+ "version": "==3.2.0"
+ },
+ "idna": {
+ "hashes": [
+ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
+ "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==3.4"
+ },
+ "pyyaml": {
+ "hashes": [
+ "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+ "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+ "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+ "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+ "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+ "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+ "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+ "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+ "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+ "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+ "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+ "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+ "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+ "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+ "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+ "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+ "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+ "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+ "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+ "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+ "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+ "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+ "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+ "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+ "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+ "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+ "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+ "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+ "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+ "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+ "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+ "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+ "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+ "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+ "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+ "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+ "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+ "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+ "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+ "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+ ],
+ "index": "pypi",
+ "version": "==6.0.1"
+ },
+ "requests": {
+ "hashes": [
+ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
+ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
+ ],
+ "index": "pypi",
+ "version": "==2.31.0"
+ },
+ "urllib3": {
+ "hashes": [
+ "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1",
+ "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.3"
+ }
+ },
+ "develop": {}
+}
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/requirements.in b/charts/k8s-gerrit/container-images/gerrit-init/requirements.in
new file mode 100644
index 0000000..a7f62a1
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/requirements.in
@@ -0,0 +1,3 @@
+setuptools
+wheel
+pipenv
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/requirements.txt b/charts/k8s-gerrit/container-images/gerrit-init/requirements.txt
new file mode 100644
index 0000000..c2de41b
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/requirements.txt
@@ -0,0 +1,46 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+# pip-compile --allow-unsafe --generate-hashes requirements.in
+#
+certifi==2022.12.7 \
+ --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \
+ --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18
+ # via pipenv
+distlib==0.3.6 \
+ --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \
+ --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e
+ # via virtualenv
+filelock==3.9.0 \
+ --hash=sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de \
+ --hash=sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d
+ # via virtualenv
+pipenv==2023.2.18 \
+ --hash=sha256:4e45226d197ad84fa11a9d944cb0e1bfcc197919944d0af96e55adf7e1fdc76c \
+ --hash=sha256:ecbe4e301616c5fa3128d557507d79a35d895cd922139969929d7357b66b1509
+ # via -r requirements.in
+platformdirs==3.0.0 \
+ --hash=sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9 \
+ --hash=sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567
+ # via virtualenv
+virtualenv==20.19.0 \
+ --hash=sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590 \
+ --hash=sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1
+ # via pipenv
+virtualenv-clone==0.5.7 \
+ --hash=sha256:418ee935c36152f8f153c79824bb93eaf6f0f7984bae31d3f48f350b9183501a \
+ --hash=sha256:44d5263bceed0bac3e1424d64f798095233b64def1c5689afa43dc3223caf5b0
+ # via pipenv
+wheel==0.38.4 \
+ --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \
+ --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8
+ # via -r requirements.in
+
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==67.4.0 \
+ --hash=sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330 \
+ --hash=sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251
+ # via
+ # -r requirements.in
+ # pipenv
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/__main__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/__main__.py
new file mode 100644
index 0000000..e49cc31
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/__main__.py
@@ -0,0 +1,18 @@
+# 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 main import main
+
+if __name__ == "__main__":
+ main()
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/__init__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/__init__.py
new file mode 100644
index 0000000..2230656
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/__init__.py
@@ -0,0 +1,13 @@
+# 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.
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/__init__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/__init__.py
new file mode 100644
index 0000000..2230656
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/__init__.py
@@ -0,0 +1,13 @@
+# 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.
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py
new file mode 100644
index 0000000..68a6f00
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py
@@ -0,0 +1,90 @@
+# 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 yaml
+
+
+class InitConfig:
+ def __init__(self):
+ self.plugins = []
+ self.libs = []
+ self.plugin_cache_enabled = False
+ self.plugin_cache_dir = None
+
+ self.ca_cert_path = True
+
+ self.is_ha = False
+ self.refdb = False
+
+ def parse(self, config_file):
+ if not os.path.exists(config_file):
+ raise FileNotFoundError(f"Could not find config file: {config_file}")
+
+ with open(config_file, "r", encoding="utf-8") as f:
+ config = yaml.load(f, Loader=yaml.SafeLoader)
+
+ if config is None:
+ raise ValueError(f"Invalid config-file: {config_file}")
+
+ if "plugins" in config:
+ self.plugins = config["plugins"]
+ if "libs" in config:
+ self.libs = config["libs"]
+ # DEPRECATED: `pluginCache` was deprecated in favor of `pluginCacheEnabled`
+ if "pluginCache" in config:
+ self.plugin_cache_enabled = config["pluginCache"]
+ if "pluginCacheEnabled" in config:
+ self.plugin_cache_enabled = config["pluginCacheEnabled"]
+ if "pluginCacheDir" in config and config["pluginCacheDir"]:
+ self.plugin_cache_dir = config["pluginCacheDir"]
+
+ if "caCertPath" in config:
+ self.ca_cert_path = config["caCertPath"]
+
+ self.is_ha = "highAvailability" in config and config["highAvailability"]
+ if "refdb" in config:
+ self.refdb = config["refdb"]
+
+ return self
+
+ def get_plugins(self):
+ return self.plugins
+
+ def get_plugin_names(self):
+ return set([p["name"] for p in self.plugins])
+
+ def get_libs(self):
+ return self.libs
+
+ def get_lib_names(self):
+ return set([p["name"] for p in self.libs])
+
+ def get_packaged_plugins(self):
+ return list(filter(lambda x: "url" not in x, self.plugins))
+
+ def get_downloaded_plugins(self):
+ return list(filter(lambda x: "url" in x, self.plugins))
+
+ def get_plugins_installed_as_lib(self):
+ return [
+ lib["name"]
+ for lib in list(
+ filter(
+ lambda x: "installAsLibrary" in x and x["installAsLibrary"],
+ self.plugins,
+ )
+ )
+ ]
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/__init__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/__init__.py
new file mode 100644
index 0000000..2230656
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/__init__.py
@@ -0,0 +1,13 @@
+# 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.
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/git.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/git.py
new file mode 100644
index 0000000..f21b28d
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/git.py
@@ -0,0 +1,76 @@
+# 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 subprocess
+
+
+class GitConfigParser:
+ def __init__(self, config_path):
+ self.path = config_path
+
+ def _execute_shell_command_and_get_output_lines(self, command):
+ sub_process_run = subprocess.run(
+ command.split(), stdout=subprocess.PIPE, check=True, universal_newlines=True
+ )
+ return [line.strip() for line in sub_process_run.stdout.splitlines()]
+
+ def _get_value(self, key):
+ command = f"git config -f {self.path} --get {key}"
+ return self._execute_shell_command_and_get_output_lines(command)
+
+ def list(self):
+ command = f"git config -f {self.path} --list"
+ options = self._execute_shell_command_and_get_output_lines(command)
+ option_list = []
+ for opt in options:
+ parsed_opt = {}
+ full_key, value = opt.split("=", 1)
+ parsed_opt["value"] = value
+ full_key = full_key.split(".")
+ parsed_opt["section"] = full_key[0]
+ if len(full_key) == 2:
+ parsed_opt["subsection"] = None
+ parsed_opt["key"] = full_key[1]
+ elif len(full_key) == 3:
+ parsed_opt["subsection"] = full_key[1]
+ parsed_opt["key"] = full_key[2]
+ option_list.append(parsed_opt)
+
+ return option_list
+
+ def get(self, key, default=None):
+ """
+ Returns value of given key in the configuration file. If the key appears
+ multiple times, the last value is returned.
+ """
+ try:
+ return self._get_value(key)[-1]
+ except subprocess.CalledProcessError:
+ return default
+
+ def get_boolean(self, key, default=False):
+ """
+ Returns boolean value of given key in the configuration file. If the key
+ appears multiple times, the last value is returned.
+ """
+ if not isinstance(default, bool):
+ raise TypeError("Default has to be a boolean.")
+
+ try:
+ value = self._get_value(key)[-1].lower()
+ if value not in ["true", "false"]:
+ raise TypeError("Value is not a boolean.")
+ return value == "true"
+ except subprocess.CalledProcessError:
+ return default
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/log.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/log.py
new file mode 100644
index 0000000..06aa72c
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/log.py
@@ -0,0 +1,26 @@
+#!/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 logging
+
+
+def get_logger(name):
+ log = logging.Logger(name)
+ handler = logging.StreamHandler()
+ handler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s %(message)s"))
+ log.addHandler(handler)
+ log.setLevel(logging.DEBUG)
+ return log
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/__init__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/__init__.py
new file mode 100644
index 0000000..2230656
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/__init__.py
@@ -0,0 +1,13 @@
+# 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.
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py
new file mode 100755
index 0000000..2c9ace0
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py
@@ -0,0 +1,372 @@
+#!/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 hashlib
+import os
+import shutil
+import time
+
+from abc import ABC, abstractmethod
+from zipfile import ZipFile
+
+import requests
+
+from ..helpers import log
+
+LOG = log.get_logger("init")
+MAX_LOCK_LIFETIME = 60
+MAX_CACHED_VERSIONS = 5
+
+REQUIRED_PLUGINS = ["healthcheck"]
+REQUIRED_HA_PLUGINS = ["high-availability"]
+REQUIRED_HA_LIBS = ["high-availability", "global-refdb"]
+
+
+class InvalidPluginException(Exception):
+ """Exception to be raised, if the downloaded plugin is not valid."""
+
+
+class MissingRequiredPluginException(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.required_plugins = self._get_required_plugins()
+ self.required_libs = self._get_required_libs()
+
+ self.plugin_dir = os.path.join(site, "plugins")
+ self.lib_dir = os.path.join(site, "lib")
+ 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 _create_lib_dir(self):
+ if not os.path.exists(self.lib_dir):
+ os.makedirs(self.lib_dir)
+ LOG.info("Created lib installation directory: %s", self.lib_dir)
+
+ def _get_installed_plugins(self):
+ return self._get_installed_jars(self.plugin_dir)
+
+ def _get_installed_libs(self):
+ return self._get_installed_jars(self.lib_dir)
+
+ @staticmethod
+ def _get_installed_jars(dir):
+ if os.path.exists(dir):
+ return [f for f in os.listdir(dir) if f.endswith(".jar")]
+
+ return []
+
+ def _get_required_plugins(self):
+ required = REQUIRED_PLUGINS.copy()
+ if self.config.is_ha:
+ required.extend(REQUIRED_HA_PLUGINS)
+ if self.config.refdb:
+ required.append(f"{self.config.refdb}-refdb")
+ LOG.info("Requiring plugins: %s", required)
+ return required
+
+ def _get_required_libs(self):
+ required = []
+ if self.config.is_ha:
+ required.extend(REQUIRED_HA_LIBS)
+ LOG.info("Requiring libs: %s", required)
+ return required
+
+ def _install_required_plugins(self):
+ for plugin in self.required_plugins:
+ if plugin in self.config.get_plugin_names():
+ continue
+
+ self._install_required_jar(plugin, self.plugin_dir)
+
+ def _install_required_libs(self):
+ for lib in self.required_libs:
+ if lib in self.config.get_lib_names():
+ continue
+
+ self._install_required_jar(lib, self.lib_dir)
+
+ def _install_required_jar(self, jar, target_dir):
+ with ZipFile("/var/war/gerrit.war", "r") as war:
+ # Lib modules can be packaged as a plugin. However, they could
+ # currently not be installed by the init pgm tool.
+ if f"WEB-INF/plugins/{jar}.jar" in war.namelist():
+ self._install_plugin_from_war(jar, target_dir)
+ return
+ try:
+ self._install_jar_from_container(jar, target_dir)
+ except FileNotFoundError:
+ raise MissingRequiredPluginException(f"Required jar {jar} was not found.")
+
+ def _install_jar_from_container(self, plugin, target_dir):
+ source_file = os.path.join("/var/plugins", plugin + ".jar")
+ target_file = os.path.join(target_dir, plugin + ".jar")
+ LOG.info(
+ "Installing plugin %s from container to %s.",
+ plugin,
+ target_file,
+ )
+ if not os.path.exists(source_file):
+ raise FileNotFoundError(
+ "Unable to find required plugin in container: " + plugin
+ )
+ if os.path.exists(target_file) and self._get_file_sha(
+ source_file
+ ) == self._get_file_sha(target_file):
+ return
+
+ shutil.copyfile(source_file, target_file)
+ self.plugins_changed = True
+
+ def _install_plugins_from_war(self):
+ for plugin in self.config.get_packaged_plugins():
+ self._install_plugin_from_war(plugin["name"], self.plugin_dir)
+
+ def _install_plugin_from_war(self, plugin, target_dir):
+ LOG.info("Installing packaged plugin %s.", plugin)
+ with ZipFile("/var/war/gerrit.war", "r") as war:
+ war.extract(f"WEB-INF/plugins/{plugin}.jar", self.plugin_dir)
+
+ source_file = f"{self.plugin_dir}/WEB-INF/plugins/{plugin}.jar"
+ target_file = os.path.join(target_dir, f"{plugin}.jar")
+ if not os.path.exists(target_file) or self._get_file_sha(
+ source_file
+ ) != self._get_file_sha(target_file):
+ os.rename(source_file, target_file)
+ self.plugins_changed = True
+
+ shutil.rmtree(os.path.join(self.plugin_dir, "WEB-INF"), ignore_errors=True)
+
+ @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 = list(self.config.get_plugins())
+ wanted_plugins.extend(self.required_plugins)
+ self._remove_unwanted(
+ wanted_plugins, self._get_installed_plugins(), self.plugin_dir
+ )
+
+ def _remove_unwanted_libs(self):
+ wanted_libs = list(self.config.get_libs())
+ wanted_libs.extend(self.required_libs)
+ wanted_libs.extend(self.config.get_plugins_installed_as_lib())
+ self._remove_unwanted(wanted_libs, self._get_installed_libs(), self.lib_dir)
+
+ @staticmethod
+ def _remove_unwanted(wanted, installed, dir):
+ for plugin in installed:
+ if os.path.splitext(plugin)[0] not in wanted:
+ os.remove(os.path.join(dir, plugin))
+ LOG.info("Removed plugin %s", plugin)
+
+ def _symlink_plugins_to_lib(self):
+ if not os.path.exists(self.lib_dir):
+ os.makedirs(self.lib_dir)
+ else:
+ for f in os.listdir(self.lib_dir):
+ path = os.path.join(self.lib_dir, f)
+ if (
+ os.path.islink(path)
+ and os.path.splitext(f)[0]
+ not in self.config.get_plugins_installed_as_lib()
+ ):
+ os.unlink(path)
+ LOG.info("Removed symlink %s", f)
+ for lib in self.config.get_plugins_installed_as_lib():
+ plugin_path = os.path.join(self.plugin_dir, f"{lib}.jar")
+ if os.path.exists(plugin_path):
+ try:
+ os.symlink(plugin_path, os.path.join(self.lib_dir, f"{lib}.jar"))
+ except FileExistsError:
+ continue
+ else:
+ raise FileNotFoundError(
+ f"Could not find plugin {lib} to symlink to lib-directory."
+ )
+
+ def execute(self):
+ self._create_plugins_dir()
+ self._create_lib_dir()
+
+ self._remove_unwanted_plugins()
+ self._remove_unwanted_libs()
+
+ self._install_required_plugins()
+ self._install_required_libs()
+
+ self._install_plugins_from_war()
+
+ for plugin in self.config.get_downloaded_plugins():
+ self._install_plugin(plugin)
+
+ for plugin in self.config.get_libs():
+ self._install_lib(plugin)
+
+ self._symlink_plugins_to_lib()
+
+ def _download_plugin(self, plugin, target):
+ LOG.info("Downloading %s plugin to %s", plugin["name"], target)
+ try:
+ response = requests.get(plugin["url"])
+ except requests.exceptions.SSLError:
+ response = requests.get(plugin["url"], verify=self.config.ca_cert_path)
+
+ 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(
+ (
+ f"SHA1 of downloaded file ({file_sha}) did not match "
+ f"expected SHA1 ({plugin['sha1']}). "
+ f"Removed downloaded file ({target})"
+ )
+ )
+
+ def _install_plugin(self, plugin):
+ self._install_jar(plugin, self.plugin_dir)
+
+ def _install_lib(self, lib):
+ self._install_jar(lib, self.lib_dir)
+
+ @abstractmethod
+ def _install_jar(self, plugin, target_dir):
+ pass
+
+
+class PluginInstaller(AbstractPluginInstaller):
+ def _install_jar(self, plugin, target_dir):
+ target = os.path.join(target_dir, f"{plugin['name']}.jar")
+ 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", encoding="utf-8") 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"],
+ f"{plugin['name']}-{plugin['sha1']}.jar",
+ )
+
+ 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"])
+ self._create_plugin_cache_dir(os.path.dirname(cached_plugin_path))
+
+ lock_path = f"{cached_plugin_path}.lock"
+ 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, cached_plugin_path)
+ finally:
+ self._remove_download_lock(lock_path)
+
+ shutil.copy(cached_plugin_path, target)
+ self._cleanup_cache(os.path.dirname(cached_plugin_path))
+
+ def _install_jar(self, plugin, target_dir):
+ install_path = os.path.join(target_dir, f"{plugin['name']}.jar")
+ 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)
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py
new file mode 100755
index 0000000..4931984
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py
@@ -0,0 +1,227 @@
+# 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 shutil
+import subprocess
+import sys
+
+from ..helpers import git, log
+from .download_plugins import get_installer
+from .reindex import IndexType, get_reindexer
+from .validate_notedb import NoteDbValidator
+
+LOG = log.get_logger("init")
+MNT_PATH = "/var/mnt"
+
+
+class GerritInit:
+ def __init__(self, site, config):
+ self.site = site
+ self.config = config
+
+ self.plugin_installer = get_installer(self.site, self.config)
+
+ self.gerrit_config = git.GitConfigParser(
+ os.path.join(MNT_PATH, "etc/config/gerrit.config")
+ )
+ self.is_online_reindex = self.gerrit_config.get_boolean(
+ "index.onlineUpgrade", True
+ )
+ self.force_offline_reindex = False
+ self.installed_plugins = self._get_installed_plugins()
+
+ self.is_replica = self.gerrit_config.get_boolean("container.replica")
+ self.pid_file = f"{self.site}/logs/gerrit.pid"
+
+ def _get_gerrit_version(self, gerrit_war_path):
+ command = f"java -jar {gerrit_war_path} version"
+ version_process = subprocess.run(
+ command.split(), stdout=subprocess.PIPE, check=True
+ )
+ return version_process.stdout.decode().strip()
+
+ def _get_installed_plugins(self):
+ plugin_path = os.path.join(self.site, "plugins")
+ installed_plugins = set()
+
+ if os.path.exists(plugin_path):
+ for f in os.listdir(plugin_path):
+ if os.path.isfile(os.path.join(plugin_path, f)) and f.endswith(".jar"):
+ installed_plugins.add(os.path.splitext(f)[0])
+
+ return installed_plugins
+
+ 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)
+ provided_version = self._get_gerrit_version("/var/war/gerrit.war")
+ LOG.info(
+ "Installed Gerrit version: %s; Provided Gerrit version: %s). ",
+ installed_version,
+ provided_version,
+ )
+ installed_minor_version = installed_version.split(".")[0:2]
+ provided_minor_version = provided_version.split(".")[0:2]
+
+ if (
+ not self.is_online_reindex
+ and installed_minor_version != provided_minor_version
+ ):
+ self.force_offline_reindex = True
+ return installed_version != provided_version
+
+ def _needs_init(self):
+ 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.")
+ return True
+
+ if self._gerrit_war_updated():
+ LOG.info("Reinitializing site to perform update.")
+ return True
+
+ if self.plugin_installer.plugins_changed:
+ LOG.info("Plugins were installed or updated. Initializing.")
+ return True
+
+ if self.config.get_plugin_names().difference(self.installed_plugins):
+ LOG.info("Reininitializing site to install additional plugins.")
+ return True
+
+ LOG.info("No initialization required.")
+ return False
+
+ def _ensure_symlink(self, src, target):
+ if not os.path.exists(src):
+ raise FileNotFoundError(f"Unable to find mounted dir: {src}")
+
+ if os.path.islink(target) and os.path.realpath(target) == src:
+ return
+
+ if os.path.exists(target):
+ if os.path.isdir(target) and not os.path.islink(target):
+ shutil.rmtree(target)
+ else:
+ os.remove(target)
+
+ os.symlink(src, target)
+
+ def _symlink_mounted_site_components(self):
+ self._ensure_symlink(f"{MNT_PATH}/git", f"{self.site}/git")
+ self._ensure_symlink(f"{MNT_PATH}/logs", f"{self.site}/logs")
+
+ mounted_shared_dir = f"{MNT_PATH}/shared"
+ if not self.is_replica and os.path.exists(mounted_shared_dir):
+ self._ensure_symlink(mounted_shared_dir, f"{self.site}/shared")
+
+ index_type = self.gerrit_config.get("index.type", default=IndexType.LUCENE.name)
+ if IndexType[index_type.upper()] is IndexType.ELASTICSEARCH:
+ self._ensure_symlink(f"{MNT_PATH}/index", f"{self.site}/index")
+
+ data_dir = f"{self.site}/data"
+ if os.path.exists(data_dir):
+ for file_or_dir in os.listdir(data_dir):
+ abs_path = os.path.join(data_dir, file_or_dir)
+ if os.path.islink(abs_path) and not os.path.exists(
+ os.path.realpath(abs_path)
+ ):
+ os.unlink(abs_path)
+ else:
+ os.makedirs(data_dir)
+
+ mounted_data_dir = f"{MNT_PATH}/data"
+ if os.path.exists(mounted_data_dir):
+ for file_or_dir in os.listdir(mounted_data_dir):
+ abs_path = os.path.join(data_dir, file_or_dir)
+ abs_mounted_path = os.path.join(mounted_data_dir, file_or_dir)
+ if os.path.isdir(abs_mounted_path):
+ self._ensure_symlink(abs_mounted_path, abs_path)
+
+ def _symlink_configuration(self):
+ etc_dir = f"{self.site}/etc"
+ if not os.path.exists(etc_dir):
+ os.makedirs(etc_dir)
+
+ for config_type in ["config", "secret"]:
+ if os.path.exists(f"{MNT_PATH}/etc/{config_type}"):
+ for file_or_dir in os.listdir(f"{MNT_PATH}/etc/{config_type}"):
+ if os.path.isfile(
+ os.path.join(f"{MNT_PATH}/etc/{config_type}", file_or_dir)
+ ):
+ self._ensure_symlink(
+ os.path.join(f"{MNT_PATH}/etc/{config_type}", file_or_dir),
+ os.path.join(etc_dir, file_or_dir),
+ )
+
+ def _remove_auto_generated_ssh_keys(self):
+ etc_dir = f"{self.site}/etc"
+ if not os.path.exists(etc_dir):
+ return
+
+ for file_or_dir in os.listdir(etc_dir):
+ full_path = os.path.join(etc_dir, file_or_dir)
+ if os.path.isfile(full_path) and file_or_dir.startswith("ssh_host_"):
+ os.remove(full_path)
+
+ def execute(self):
+ if not self.is_replica:
+ self._symlink_mounted_site_components()
+ elif not NoteDbValidator(MNT_PATH).check():
+ LOG.info("NoteDB not ready. Initializing repositories.")
+ self._symlink_mounted_site_components()
+ self._symlink_configuration()
+
+ if os.path.exists(self.pid_file):
+ os.remove(self.pid_file)
+
+ self.plugin_installer.execute()
+
+ if self._needs_init():
+ if self.gerrit_config:
+ LOG.info("Existing gerrit.config found.")
+ dev_option = (
+ "--dev"
+ if self.gerrit_config.get(
+ "auth.type", "development_become_any_account"
+ ).lower()
+ == "development_become_any_account"
+ else ""
+ )
+ else:
+ LOG.info("No gerrit.config found. Initializing default site.")
+ dev_option = "--dev"
+
+ flags = f"--no-auto-start --batch {dev_option}"
+
+ command = f"java -jar /var/war/gerrit.war init {flags} -d {self.site}"
+
+ init_process = subprocess.run(
+ command.split(), stdout=subprocess.PIPE, check=True
+ )
+
+ if init_process.returncode > 0:
+ LOG.error(
+ "An error occurred, when initializing Gerrit. Exit code: %d",
+ init_process.returncode,
+ )
+ sys.exit(1)
+
+ self._remove_auto_generated_ssh_keys()
+ self._symlink_configuration()
+
+ if self.is_replica:
+ self._symlink_mounted_site_components()
+
+ get_reindexer(self.site, self.config).start(self.force_offline_reindex)
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/reindex.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/reindex.py
new file mode 100755
index 0000000..e5ec6df
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/reindex.py
@@ -0,0 +1,208 @@
+#!/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 abc
+import enum
+import os.path
+import subprocess
+import sys
+
+import requests
+
+from ..helpers import git, log
+
+LOG = log.get_logger("reindex")
+MNT_PATH = "/var/mnt"
+INDEXES_PRIMARY = set(["accounts", "changes", "groups", "projects"])
+INDEXES_REPLICA = set(["groups"])
+
+
+class IndexType(enum.Enum):
+ LUCENE = enum.auto()
+ ELASTICSEARCH = enum.auto()
+
+
+class GerritAbstractReindexer(abc.ABC):
+ def __init__(self, gerrit_site_path, config):
+ self.gerrit_site_path = gerrit_site_path
+ self.index_config_path = f"{self.gerrit_site_path}/index/gerrit_index.config"
+ self.init_config = config
+
+ self.gerrit_config = git.GitConfigParser(
+ os.path.join(MNT_PATH, "etc/config/gerrit.config")
+ )
+ self.is_online_reindex = self.gerrit_config.get_boolean(
+ "index.onlineUpgrade", True
+ )
+ self.is_replica = self.gerrit_config.get_boolean("container.replica", False)
+
+ self.configured_indices = self._parse_gerrit_index_config()
+
+ @abc.abstractmethod
+ def _get_indices(self):
+ pass
+
+ def _parse_gerrit_index_config(self):
+ indices = {}
+ if os.path.exists(self.index_config_path):
+ config = git.GitConfigParser(self.index_config_path)
+ options = config.list()
+ for opt in options:
+ name, version = opt["subsection"].rsplit("_", 1)
+ ready = opt["value"].lower() == "true"
+ if name in indices:
+ indices[name] = {
+ "read": version if ready else indices[name]["read"],
+ "latest_write": max(version, indices[name]["latest_write"]),
+ }
+ else:
+ indices[name] = {
+ "read": version if ready else None,
+ "latest_write": version,
+ }
+ return indices
+
+ def _get_not_ready_indices(self):
+ not_ready_indices = []
+ for index, index_attrs in self.configured_indices.items():
+ if not index_attrs["read"]:
+ LOG.info("Index %s not ready.", index)
+ not_ready_indices.append(index)
+ index_set = INDEXES_REPLICA if self.is_replica else INDEXES_PRIMARY
+ not_ready_indices.extend(index_set.difference(self.configured_indices.keys()))
+ return not_ready_indices
+
+ def _indexes_need_update(self):
+ indices = self._get_indices()
+
+ if not indices:
+ return True
+
+ for index, index_attrs in self.configured_indices.items():
+ if (
+ index not in indices
+ or index_attrs["latest_write"] != indices[index]
+ or index_attrs["read"] != index_attrs["latest_write"]
+ ):
+ return True
+ return False
+
+ def reindex(self, indices=None):
+ LOG.info("Starting to reindex.")
+ command = f"java -jar /var/war/gerrit.war reindex -d {self.gerrit_site_path}"
+
+ if indices:
+ command += " ".join([f" --index {i}" for i in indices])
+
+ reindex_process = subprocess.run(
+ command.split(), stdout=subprocess.PIPE, check=True
+ )
+
+ if reindex_process.returncode > 0:
+ LOG.error(
+ "An error occurred, when reindexing Gerrit indices. Exit code: %d",
+ reindex_process.returncode,
+ )
+ sys.exit(1)
+
+ LOG.info("Finished reindexing.")
+
+ def start(self, is_forced):
+ if is_forced:
+ self.reindex()
+ return
+
+ if not self.configured_indices:
+ LOG.info("gerrit_index.config does not exist. Creating all indices.")
+ self.reindex()
+ return
+
+ not_ready_indices = self._get_not_ready_indices()
+ if not_ready_indices:
+ self.reindex(not_ready_indices)
+
+ if not self.is_online_reindex and self._indexes_need_update():
+ LOG.info("Not all indices are up-to-date.")
+ self.reindex()
+ return
+
+ LOG.info("Skipping reindexing.")
+
+
+class GerritLuceneReindexer(GerritAbstractReindexer):
+ def _get_indices(self):
+ file_list = os.listdir(os.path.join(self.gerrit_site_path, "index"))
+ file_list.remove("gerrit_index.config")
+ lucene_indices = {}
+ for index in file_list:
+ try:
+ (name, version) = index.split("_")
+ if name in lucene_indices:
+ lucene_indices[name] = max(version, lucene_indices[name])
+ else:
+ lucene_indices[name] = version
+ except ValueError:
+ LOG.debug("Ignoring invalid file in index-directory: %s", index)
+ return lucene_indices
+
+
+class GerritElasticSearchReindexer(GerritAbstractReindexer):
+ def _get_elasticsearch_config(self):
+ es_config = {}
+ gerrit_config = git.GitConfigParser(
+ os.path.join(self.gerrit_site_path, "etc", "gerrit.config")
+ )
+ es_config["prefix"] = gerrit_config.get(
+ "elasticsearch.prefix", default=""
+ ).lower()
+ es_config["server"] = gerrit_config.get(
+ "elasticsearch.server", default=""
+ ).lower()
+ return es_config
+
+ def _get_indices(self):
+ es_config = self._get_elasticsearch_config()
+ url = f"{es_config['server']}/{es_config['prefix']}*"
+ try:
+ response = requests.get(url)
+ except requests.exceptions.SSLError:
+ response = requests.get(url, verify=self.init_config.ca_cert_path)
+
+ es_indices = {}
+ for index, _ in response.json().items():
+ try:
+ index = index.replace(es_config["prefix"], "", 1)
+ (name, version) = index.split("_")
+ es_indices[name] = version
+ except ValueError:
+ LOG.debug("Found unknown index: %s", index)
+
+ return es_indices
+
+
+def get_reindexer(gerrit_site_path, config):
+ gerrit_config = git.GitConfigParser(
+ os.path.join(gerrit_site_path, "etc", "gerrit.config")
+ )
+ index_type = gerrit_config.get("index.type", default=IndexType.LUCENE.name)
+
+ if IndexType[index_type.upper()] is IndexType.LUCENE:
+ return GerritLuceneReindexer(gerrit_site_path, config)
+
+ if IndexType[index_type.upper()] is IndexType.ELASTICSEARCH:
+ return GerritElasticSearchReindexer(gerrit_site_path, config)
+
+ raise RuntimeError(f"Unknown index type {index_type}.")
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/validate_notedb.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/validate_notedb.py
new file mode 100644
index 0000000..aff9ce6
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/validate_notedb.py
@@ -0,0 +1,74 @@
+#!/usr/bin/python3
+# 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 subprocess
+import time
+
+from ..helpers import log
+
+LOG = log.get_logger("init")
+
+
+class NoteDbValidator:
+ def __init__(self, site):
+ self.site = site
+
+ self.notedb_repos = ["All-Projects.git", "All-Users.git"]
+ self.required_refs = {
+ "All-Projects.git": ["refs/meta/config", "refs/meta/version"],
+ "All-Users.git": ["refs/meta/config"],
+ }
+
+ def _test_repo_exists(self, repo):
+ return os.path.exists(os.path.join(self.site, "git", repo))
+
+ def _test_ref_exists(self, repo, ref):
+ command = f"git --git-dir {self.site}/git/{repo} rev-parse --verify {ref}"
+ git_show_ref = subprocess.run(
+ command.split(),
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ check=False,
+ )
+
+ return git_show_ref.returncode == 0
+
+ def wait_until_valid(self):
+ for repo in self.notedb_repos:
+ LOG.info("Waiting for repository %s.", repo)
+ while not self._test_repo_exists(repo):
+ time.sleep(1)
+ LOG.info("Found %s.", repo)
+
+ for ref in self.required_refs[repo]:
+ LOG.info("Waiting for ref %s in repository %s.", ref, repo)
+ while not self._test_ref_exists(repo, ref):
+ time.sleep(1)
+ LOG.info("Found ref %s in repo %s.", ref, repo)
+
+ def check(self):
+ for repo in self.notedb_repos:
+ if not self._test_repo_exists(repo):
+ LOG.info("Repository %s is missing.", repo)
+ return False
+ LOG.info("Found %s.", repo)
+
+ for ref in self.required_refs[repo]:
+ if not self._test_ref_exists(repo, ref):
+ LOG.info("Ref %s in repository %s is missing.", ref, repo)
+ return False
+ LOG.info("Found ref %s in repo %s.", ref, repo)
+ return True
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/main.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/main.py
new file mode 100755
index 0000000..b41cf3a
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/main.py
@@ -0,0 +1,93 @@
+#!/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
+
+from initializer.tasks import download_plugins, init, reindex, validate_notedb
+from initializer.config.init_config import InitConfig
+
+
+def _run_download_plugins(args):
+ config = InitConfig().parse(args.config)
+ download_plugins.get_installer(args.site, config).execute()
+
+
+def _run_init(args):
+ config = InitConfig().parse(args.config)
+ init.GerritInit(args.site, config).execute()
+
+
+def _run_reindex(args):
+ config = InitConfig().parse(args.config)
+ reindex.get_reindexer(args.site, config).start(args.force)
+
+
+def _run_validate_notedb(args):
+ validate_notedb.NoteDbValidator(args.site).wait_until_valid()
+
+
+def 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,
+ )
+
+ subparsers = parser.add_subparsers()
+
+ parser_download_plugins = subparsers.add_parser(
+ "download-plugins", help="Download plugins"
+ )
+ parser_download_plugins.set_defaults(func=_run_download_plugins)
+
+ parser_init = subparsers.add_parser("init", help="Initialize Gerrit site")
+ parser_init.set_defaults(func=_run_init)
+
+ parser_reindex = subparsers.add_parser("reindex", help="Reindex Gerrit indexes")
+ parser_reindex.add_argument(
+ "-f",
+ "--force",
+ help="Reindex even if indices are ready.",
+ dest="force",
+ action="store_true",
+ )
+ parser_reindex.set_defaults(func=_run_reindex)
+
+ parser_validate_notedb = subparsers.add_parser(
+ "validate-notedb", help="Validate NoteDB"
+ )
+ parser_validate_notedb.set_defaults(func=_run_validate_notedb)
+
+ args = parser.parse_args()
+ args.func(args)
+
+
+if __name__ == "__main__":
+ main()