From 896a57288b5c8266d5e46bb73259d4ef7a1b1a27 Mon Sep 17 00:00:00 2001 From: Aleksander Mierzwicki Date: Mon, 23 Mar 2026 16:54:15 +0100 Subject: [PATCH] DEVPROD-3811 Added support for OpenSearch for Jira. --- .github/workflows/kind.yaml | 5 + docs/docs/containers/JIRA.md | 128 ++++++++++++++++++ docs/docs/examples/.pages | 1 + docs/docs/examples/jira/.pages | 4 + docs/docs/examples/jira/JIRA_OPENSEARCH.md | 96 +++++++++++++ docs/docs/userguide/CONFIGURATION.md | 52 +++++++ src/main/charts/jira/Chart.lock | 7 +- src/main/charts/jira/Chart.yaml | 4 + src/main/charts/jira/templates/_helpers.tpl | 54 ++++++++ .../jira/templates/secret-opensearch.yaml | 11 ++ .../charts/jira/templates/statefulset.yaml | 21 +++ src/main/charts/jira/values.yaml | 53 ++++++++ src/test/config/jira/values.yaml | 6 + .../test/AdditionalConfigPropertiesTest.java | 72 ++++++++++ src/test/java/test/JiraOpenSearchTest.java | 79 +++++++++++ .../expected_helm_output/jira/output.yaml | 22 ++- src/test/scripts/helm_install.sh | 2 +- src/test/scripts/kind/deploy_app.sh | 81 +++++++++++ 18 files changed, 694 insertions(+), 4 deletions(-) create mode 100644 docs/docs/examples/jira/.pages create mode 100644 docs/docs/examples/jira/JIRA_OPENSEARCH.md create mode 100644 src/main/charts/jira/templates/secret-opensearch.yaml create mode 100644 src/test/java/test/AdditionalConfigPropertiesTest.java create mode 100644 src/test/java/test/JiraOpenSearchTest.java diff --git a/.github/workflows/kind.yaml b/.github/workflows/kind.yaml index d35268acd..43b808ad1 100644 --- a/.github/workflows/kind.yaml +++ b/.github/workflows/kind.yaml @@ -85,6 +85,11 @@ jobs: sleep 10 kubectl wait --for=condition=ready pod/${DC_APP}-1 -n atlassian --timeout=360s + - name: Verify ${{inputs.dc_app}} OpenSearch integration + run: | + source src/test/scripts/kind/deploy_app.sh + verify_opensearch + - name: Verify ${{inputs.dc_app}} metrics availability run: | source src/test/scripts/kind/deploy_app.sh diff --git a/docs/docs/containers/JIRA.md b/docs/docs/containers/JIRA.md index 632b83a83..1bc18a27d 100644 --- a/docs/docs/containers/JIRA.md +++ b/docs/docs/containers/JIRA.md @@ -219,6 +219,55 @@ information, please refer to Override the default AWS API endpoint with a custom one (optional). +### OpenSearch configuration + +Starting with Jira 11.2, you can configure Jira to use OpenSearch as the search platform. +For the full list of available OpenSearch properties and requirements, see +[Configuring OpenSearch for Jira](https://confluence.atlassian.com/adminjiraserver/configuring-opensearch-for-jira-1620511851.html). + +OpenSearch properties are injected into `jira-config.properties` using +`ADDITIONAL_JIRA_CONFIG_*` environment variables (see +[Custom `jira-config.properties`](#custom-jira-configproperties) for details on +the mechanism). + +The minimum required properties are: + +* `search.platform` + + The search platform to use. Set to `opensearch` to enable OpenSearch. + +* `opensearch.http.url` + + HTTP(S) URL of the OpenSearch cluster, or multiple URLs separated by commas. + +* `opensearch.username` + + Username for the OpenSearch cluster. + +* `opensearch.password` + + Password for the OpenSearch cluster. + +The `__EXPAND_ENV` suffix lets you keep the password in a separate environment +variable rather than embedding it directly in the property line. In the example +below `MY_OPENSEARCH_PASSWORD` is passed inline for brevity: + +```bash +docker run \ + -e MY_OPENSEARCH_PASSWORD=my-secret \ + -e ADDITIONAL_JIRA_CONFIG_01="search.platform=opensearch" \ + -e ADDITIONAL_JIRA_CONFIG_02="opensearch.http.url=http://opensearch-host:9200" \ + -e ADDITIONAL_JIRA_CONFIG_03="opensearch.username=admin" \ + -e ADDITIONAL_JIRA_CONFIG_04__EXPAND_ENV="opensearch.password={MY_OPENSEARCH_PASSWORD}" \ + atlassian/jira-software:latest +``` + +!!! warning "Sensitive values on the command line" + Passing secrets via `-e` exposes them in shell history, process listings, + and `docker inspect` output. In production, use `--env-file` with a + permission-protected file or an external secrets manager to supply + `MY_OPENSEARCH_PASSWORD`. + ### S3 Attachments storage configuration Starting with Jira 9.9, you can configure Jira to [store attachment files in Amazon S3](https://confluence.atlassian.com/adminjiraserver/storing-attachments-in-amazon-s3-1282250191.html). For requirements and additional information, please refer to [Configuring Amazon S3 Object Storage](https://confluence.atlassian.com/pages/viewpage.action?spaceKey=JSERVERM&title=.Configuring+Amazon+S3+object+storage+vJira_admin_9.9). @@ -523,6 +572,85 @@ as a non-root user. The default Tomcat session timeout (in minutes) for all newly created sessions which is set in web.xml. Defaults to 30. +### Custom `jira-config.properties` + +Additional properties can be injected into `jira-config.properties` using +environment variables prefixed with `ADDITIONAL_JIRA_CONFIG_`. + +Each variable's value must be a complete property line in `key=value` format. +Environment variable names are sorted for consistent file generation; order has +no effect on runtime behavior. + +```bash +docker run \ + -e ADDITIONAL_JIRA_CONFIG_01="jira.websudo.is.disabled=true" \ + -e ADDITIONAL_JIRA_CONFIG_02="jira.lf.top.bgcolour=#003366" \ + atlassian/jira-software:latest +``` + +#### How properties are written + +The properties are written to a clearly marked auto-generated section at the end +of the file. Any manually added content outside this section is preserved across +container restarts. + +#### Injecting secrets via `__EXPAND_ENV` + +For values that reference secrets stored in separate environment variables +(common in Kubernetes where secrets are mounted as env vars), use the +`__EXPAND_ENV` suffix. Placeholders in `{VAR_NAME}` format are replaced with +the corresponding environment variable value at startup: + +```bash +docker run \ + -e MY_OPENSEARCH_PASSWORD=my-secret \ + -e ADDITIONAL_JIRA_CONFIG_01="search.platform=opensearch" \ + -e ADDITIONAL_JIRA_CONFIG_02__EXPAND_ENV="opensearch.password={MY_OPENSEARCH_PASSWORD}" \ + atlassian/jira-software:latest +``` + +This generates the following in `jira-config.properties`: + +```properties +# ---- AUTO GENERATED ADDITIONAL PROPERTIES FROM DOCKER IMAGE --- +# DO NOT MODIFY this section - it is auto-generated during container startup +# from ADDITIONAL_JIRA_CONFIG_* environment variables +search.platform=opensearch +opensearch.password=my-secret +# ---- END OF AUTO GENERATED ADDITIONAL PROPERTIES --- +``` + +If a referenced environment variable is not set, the placeholder is left +unchanged and a warning is logged. + +#### Limitations and requirements + +* **Local home directory only**: `jira-config.properties` is written to + `$JIRA_HOME` which must be the node-local home directory (not shared storage). + Using `ADDITIONAL_JIRA_CONFIG_*` when `jira-config.properties` is stored on a + shared filesystem (e.g. NFS, EFS) may cause unexpected outcomes due to update + races across different container instances during rolling deployments. + +* **Read-only mounts not supported**: If `jira-config.properties` is mounted as + a read-only file (e.g. via a Kubernetes ConfigMap volume mount), the + `ADDITIONAL_JIRA_CONFIG_*` variables will have no effect. A warning is logged + in this case. Manage the file content entirely through the mount instead — do + not combine both approaches. + +* **Auto-generated section**: The generated properties are placed inside clearly + marked comment boundaries at the end of the file. Manual edits outside these + markers are preserved. Do not edit content within the markers — it will be + overwritten on the next container startup. + +| Aspect | Detail | +|-------------------------|-------------------------------------------------------------| +| Env var prefix | `ADDITIONAL_JIRA_CONFIG_` | +| Secret expansion suffix | `__EXPAND_ENV` (double underscore) | +| Target file | `$JIRA_HOME/jira-config.properties` | +| Ordering | Sorted for reproducibility; order has no effect on behavior | +| Existing content | Preserved (only auto-generated section is replaced) | +| No matching env vars | File is not created; stale section is removed if present | + ### Advanced Configuration As mentioned at the top of this section, the settings from the environment are diff --git a/docs/docs/examples/.pages b/docs/docs/examples/.pages index 204a75031..4fb56f86a 100644 --- a/docs/docs/examples/.pages +++ b/docs/docs/examples/.pages @@ -8,5 +8,6 @@ nav: - bamboo - bitbucket - confluence + - jira - logging - ... diff --git a/docs/docs/examples/jira/.pages b/docs/docs/examples/jira/.pages new file mode 100644 index 000000000..6e86fad08 --- /dev/null +++ b/docs/docs/examples/jira/.pages @@ -0,0 +1,4 @@ +collapse_single_pages: false +nav: + - JIRA_OPENSEARCH.md + - ... diff --git a/docs/docs/examples/jira/JIRA_OPENSEARCH.md b/docs/docs/examples/jira/JIRA_OPENSEARCH.md new file mode 100644 index 000000000..f5e9fe9b2 --- /dev/null +++ b/docs/docs/examples/jira/JIRA_OPENSEARCH.md @@ -0,0 +1,96 @@ +# Configuring OpenSearch for Jira + +!!!info "Jira and Helm chart version" + OpenSearch is supported in Jira 11.2 and Helm chart 2.0.10 onwards. + +As Jira instances grow in size and scale, the default search engine, Lucene, may be slower to index and return search results. To address this, Jira Data Center offers an alternative search engine as an opt-in feature — OpenSearch. + +## Deploy OpenSearch Helm Chart with Jira + +!!!warning "Support disclaimer" + Atlassian does not officially support OpenSearch Helm chart that can be installed with the Jira Helm release. Should you encounter any issues with the deployment, maintenance and upgrades, reach out to the [vendor](https://github.com/opensearch-project/helm-charts/tree/main/charts/opensearch){.external}. + Moreover, if you intend to deploy OpenSearch to a critical Kubernetes environment, make sure you follow all the best practices, i.e. deploy a multi node cluster, use taints and tolerations, affinity rules, sufficient resources requests, have DR and backup strategies etc. + +## Deploy with the default settings + +To deploy OpenSearch Helm chart and automatically configure Jira to use it as a search platform, set the following in your Helm values file: + +```yaml +opensearch: + enabled: true +``` +This will: + +* auto-generate the initial OpenSearch admin password and create a Kubernetes secret with `OPENSEARCH_INITIAL_ADMIN_PASSWORD` key +* deploy [OpenSearch Helm chart](https://github.com/opensearch-project/helm-charts/tree/main/charts/opensearch){.external} to the target namespace with the default settings: single node, 1Gi memory/1 vCPU resources requests, 10Gi storage request +* configure Jira to use the deployed OpenSearch cluster by setting opensearch properties, which are written to `jira-config.properties` at startup. The following properties are configured: `search.platform=opensearch`, `opensearch.http.url=http://opensearch-cluster-master:9200`, `opensearch.username=admin`, and `opensearch.password`. + +## Override OpenSearch Helm chart values + +You can configure your OpenSearch cluster and the deployment options by overriding any values that the [Helm chart](https://github.com/opensearch-project/helm-charts/blob/main/charts/opensearch/values.yaml){.external} exposes. OpenSearch values must be nested under `opensearch` stanza in your Helm values file, for example: + +```yaml +opensearch: + singleNode: false + replicas: 5 + config: + opensearch.yml: | + cluster.name: opensearch-cluster +``` + +## Use an existing OpenSearch secret + +If you have a pre-created Kubernetes secret with the OpenSearch admin password, you can reference it instead of having the chart auto-generate one: + +```yaml +opensearch: + enabled: true + credentials: + createSecret: false + existingSecretRef: + name: my-opensearch-secret +``` + +The secret must contain a key named `OPENSEARCH_INITIAL_ADMIN_PASSWORD`. + +## Connect to an external OpenSearch instance + +If you already have an OpenSearch cluster running outside of Kubernetes (or +managed separately), you can configure Jira to use it without deploying the +bundled OpenSearch Helm chart. OpenSearch properties are written to +`jira-config.properties` at container startup via the +`additionalConfigProperties` mechanism (see +[Additional config properties](../../userguide/CONFIGURATION.md#additional-config-properties)). + +First, create a Kubernetes Secret containing the OpenSearch password: + +```bash +kubectl create secret generic opensearch-credentials \ + --from-literal=password='' +``` + +Then reference it in your Helm values file. The `additionalEnvironmentVariables` +entry injects the secret as an environment variable, and +`additionalConfigPropertiesExpandEnv` expands it into the property value at +startup: + +```yaml +jira: + additionalEnvironmentVariables: + - name: MY_OPENSEARCH_PASSWORD + valueFrom: + secretKeyRef: + name: opensearch-credentials + key: password + + additionalConfigProperties: + - "search.platform=opensearch" + - "opensearch.http.url=http://opensearch-host:9200" + - "opensearch.username=admin" + + additionalConfigPropertiesExpandEnv: + - "opensearch.password={MY_OPENSEARCH_PASSWORD}" +``` + +For the full list of available OpenSearch properties, see +[Configuring OpenSearch for Jira](https://confluence.atlassian.com/adminjiraserver/configuring-opensearch-for-jira-1620511851.html){.external}. diff --git a/docs/docs/userguide/CONFIGURATION.md b/docs/docs/userguide/CONFIGURATION.md index f18c06f3d..482605c14 100644 --- a/docs/docs/userguide/CONFIGURATION.md +++ b/docs/docs/userguide/CONFIGURATION.md @@ -419,6 +419,58 @@ jira: * dash `-` becomes underscore `_` * Example: `this.new-property` becomes `THIS_NEW_PROPERTY` +## :material-book-cog: Additional config properties + +!!!info "Jira only" + This feature is currently available for Jira only. It requires a Jira + container image version that supports the `ADDITIONAL_JIRA_CONFIG_*` + environment variables. See the + [Docker image documentation](../containers/JIRA.md#custom-jira-configproperties) + for full details on the underlying mechanism. + +The Helm chart provides dedicated values for injecting properties into +`jira-config.properties` without constructing environment variable names +manually: + +```yaml +jira: + additionalConfigProperties: + - "jira.websudo.is.disabled=true" + - "jira.lf.top.bgcolour=#003366" +``` + +These are equivalent to setting `ADDITIONAL_JIRA_CONFIG_*` environment +variables directly. The values can also be set via `--set`: + +```bash +helm install jira atlassian-data-center/jira \ + --set 'jira.additionalConfigProperties[0]=jira.websudo.is.disabled=true' +``` + +### Injecting secrets + +For values that reference Kubernetes Secrets (e.g. passwords), use +`additionalConfigPropertiesExpandEnv`. Placeholders in `{VAR_NAME}` format are +replaced with the corresponding environment variable value at container +startup: + +```yaml +jira: + additionalEnvironmentVariables: + - name: MY_SECRET + valueFrom: + secretKeyRef: + name: my-k8s-secret + key: password + + additionalConfigPropertiesExpandEnv: + - "some.password={MY_SECRET}" +``` + +Alternatively, you can use `jira.additionalEnvironmentVariables` to pass the +`ADDITIONAL_JIRA_CONFIG_*` environment variables explicitly if you need full +control over naming. + ## :material-book-cog: Additional libraries & plugins The products' Docker images contain the default set of bundled libraries and plugins. diff --git a/src/main/charts/jira/Chart.lock b/src/main/charts/jira/Chart.lock index a6392166b..56181c33a 100644 --- a/src/main/charts/jira/Chart.lock +++ b/src/main/charts/jira/Chart.lock @@ -2,5 +2,8 @@ dependencies: - name: common repository: https://atlassian.github.io/data-center-helm-charts version: 1.2.7 -digest: sha256:6dc6e131380a4f43edcaae60ee0f8341a463013d8460bd657ca798139d4f428a -generated: "2024-09-10T03:31:09.693286348Z" +- name: opensearch + repository: https://opensearch-project.github.io/helm-charts + version: 3.5.0 +digest: sha256:1bd020af24c471b52a62f6b9330a0f1d94e27a60087fa48d6f1f152e91bec9ea +generated: "2026-03-13T14:50:45.468195+01:00" diff --git a/src/main/charts/jira/Chart.yaml b/src/main/charts/jira/Chart.yaml index 538ddf9f1..46e794287 100644 --- a/src/main/charts/jira/Chart.yaml +++ b/src/main/charts/jira/Chart.yaml @@ -28,3 +28,7 @@ dependencies: - name: common version: 1.2.7 repository: https://atlassian.github.io/data-center-helm-charts +- name: opensearch + version: 3.5.0 + repository: https://opensearch-project.github.io/helm-charts + condition: opensearch.enabled diff --git a/src/main/charts/jira/templates/_helpers.tpl b/src/main/charts/jira/templates/_helpers.tpl index 4f9cc1b20..18768bd1d 100644 --- a/src/main/charts/jira/templates/_helpers.tpl +++ b/src/main/charts/jira/templates/_helpers.tpl @@ -289,6 +289,20 @@ Define additional environment variables here to allow template overrides when us {{- end }} {{- end }} +{{/* +Renders ADDITIONAL_JIRA_CONFIG_* environment variables from additionalConfigProperties values +*/}} +{{- define "jira.additionalConfigProperties" -}} +{{- range $index, $prop := .Values.jira.additionalConfigProperties }} +- name: {{ printf "ADDITIONAL_JIRA_CONFIG_HELM_%03d" $index }} + value: {{ $prop | quote }} +{{- end }} +{{- range $index, $prop := .Values.jira.additionalConfigPropertiesExpandEnv }} +- name: {{ printf "ADDITIONAL_JIRA_CONFIG_HELM_%03d__EXPAND_ENV" $index }} + value: {{ $prop | quote }} +{{- end }} +{{- end }} + {{/* For each additional library declared, generate a volume mount that injects that library into the Jira lib directory */}} @@ -545,3 +559,43 @@ volumeClaimTemplates: set -e; cp $JAVA_HOME/lib/security/cacerts /var/ssl/cacerts; chmod 664 /var/ssl/cacerts; for crt in /tmp/crt/*.*; do echo "Adding $crt to keystore"; keytool -import -keystore /var/ssl/cacerts -storepass changeit -noprompt -alias $(echo $(basename $crt)) -file $crt; done; {{- end }} {{- end }} + +{{- define "generate_static_password_b64enc" -}} +{{- if not (index .Release "temp_vars") -}} +{{- $_ := set .Release "temp_vars" dict -}} +{{- end -}} +{{- $key := printf "%s_%s" .Release.Name "password" -}} +{{- if not (index .Release.temp_vars $key) -}} +{{- $_ := set .Release.temp_vars $key (randAlphaNum 40 | b64enc ) -}} +{{- end -}} +{{- index .Release.temp_vars $key -}} +{{- end -}} + +{{- define "opensearch.initial.admin.password" }} +{{- $defaultSecretName := "opensearch-initial-password" }} +{{- $secretName := default $defaultSecretName .Values.opensearch.credentials.existingSecretRef.name }} +{{- $secretData := (lookup "v1" "Secret" .Release.Namespace $secretName) }} +{{- if $secretData.data }} +{{- index $secretData.data "OPENSEARCH_INITIAL_ADMIN_PASSWORD" }} +{{- else }} +{{ include "generate_static_password_b64enc" . }} +{{- end }} +{{- end }} + +{{- define "opensearch.env.vars" }} +{{- if .Values.opensearch.enabled }} +- name: ADDITIONAL_JIRA_CONFIG_SEARCH_PLATFORM + value: "search.platform=opensearch" +- name: ADDITIONAL_JIRA_CONFIG_SEARCH_URL + value: "opensearch.http.url=http://opensearch-cluster-master:9200" +- name: ADDITIONAL_JIRA_CONFIG_SEARCH_USERNAME + value: "opensearch.username=admin" +- name: OPENSEARCH_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.opensearch.credentials.existingSecretRef.name | default "opensearch-initial-password" }} + key: OPENSEARCH_INITIAL_ADMIN_PASSWORD +- name: ADDITIONAL_JIRA_CONFIG_SEARCH_PASSWORD__EXPAND_ENV + value: "opensearch.password={OPENSEARCH_ADMIN_PASSWORD}" +{{- end }} +{{- end }} diff --git a/src/main/charts/jira/templates/secret-opensearch.yaml b/src/main/charts/jira/templates/secret-opensearch.yaml new file mode 100644 index 000000000..c5f85d623 --- /dev/null +++ b/src/main/charts/jira/templates/secret-opensearch.yaml @@ -0,0 +1,11 @@ +{{- if and .Values.opensearch.enabled .Values.opensearch.credentials.createSecret (empty .Values.opensearch.credentials.existingSecretRef.name) }} +apiVersion: v1 +kind: Secret +metadata: + name: opensearch-initial-password + labels: + {{- include "common.labels.commonLabels" . | nindent 4 }} +type: Opaque +data: + OPENSEARCH_INITIAL_ADMIN_PASSWORD: {{- include "opensearch.initial.admin.password" . | indent 4 }} +{{- end }} diff --git a/src/main/charts/jira/templates/statefulset.yaml b/src/main/charts/jira/templates/statefulset.yaml index bf92fe615..ffb0220ed 100644 --- a/src/main/charts/jira/templates/statefulset.yaml +++ b/src/main/charts/jira/templates/statefulset.yaml @@ -59,6 +59,25 @@ spec: {{- end }} {{- end }} initContainers: + {{- if .Values.opensearch.enabled }} + - name: opensearch-check + image: {{ include "jira.image" . | quote }} + command: ['sh', '-c'] + args: + - | + timeout=300 + end=$(($(date +%s) + timeout)) + while [ $(date +%s) -lt $end ]; do + if curl -s -o /dev/null -w "%{http_code}" http://opensearch-cluster-master:9200 | grep -qE '^(200|401|403)$'; then + echo "OpenSearch is ready" + exit 0 + fi + echo "OpenSearch server not ready or not reachable. Waiting..." + sleep 5 + done + echo "OpenSearch did not become ready in ${timeout} seconds. Exiting" + exit 1 + {{- end }} {{- include "jira.additionalInitContainers" . | nindent 8 }} {{- if and .Values.volumes.sharedHome.nfsPermissionFixer.enabled (not .Values.openshift.runWithRestrictedSCC) }} - name: nfs-permission-fixer @@ -168,6 +187,8 @@ spec: - name: CATALINA_OPTS value: {{ include "common.jmx.javaagent" . | replace "\n" "" | quote }} {{- end }} + {{- include "opensearch.env.vars" . | nindent 12 }} + {{- include "jira.additionalConfigProperties" . | nindent 12 }} {{- include "jira.additionalEnvironmentVariables" . | nindent 12 }} ports: - name: http diff --git a/src/main/charts/jira/values.yaml b/src/main/charts/jira/values.yaml index 8ef066712..46178aefc 100644 --- a/src/main/charts/jira/values.yaml +++ b/src/main/charts/jira/values.yaml @@ -907,6 +907,25 @@ jira: # additionalVolumeMounts: [] + # -- Additional properties to inject into jira-config.properties at container + # startup via ADDITIONAL_JIRA_CONFIG_* environment variables. + # Each entry is a "key=value" property line. Environment variable names are + # sorted for consistent file generation; order has no effect on runtime behavior. + # + # Requires a Jira container image version that supports ADDITIONAL_JIRA_CONFIG_*. + # + additionalConfigProperties: [] + # - "jira.websudo.is.disabled=true" + # - "jira.lf.top.bgcolour=#003366" + + # -- Additional properties requiring environment variable expansion at + # container startup. Values containing {VAR_NAME} placeholders will be + # replaced with the corresponding environment variable value. + # Useful for injecting secrets without hardcoding them in values. + # + additionalConfigPropertiesExpandEnv: [] + # - "opensearch.password={OPENSEARCH_ADMIN_PASSWORD}" + # -- Defines any additional environment variables to be passed to the Jira # container. See https://hub.docker.com/r/atlassian/jira-software for # supported variables. @@ -1347,3 +1366,37 @@ openshift: # configuration files as ConfigMaps. # runWithRestrictedSCC: false + +opensearch: + + # -- Deploy OpenSearch Helm chart and Configure Jira to use it as a search platform + # + enabled: false + + credentials: + # -- Let the Helm chart create a secret with an auto generated initial admin password + # + createSecret: true + + # -- Use an existing secret with the key OPENSEARCH_INITIAL_ADMIN_PASSWORD holding the initial admin password + # + existingSecretRef: + name: + + # -- OpenSearch helm specific values, see: https://github.com/opensearch-project/helm-charts/blob/main/charts/opensearch/values.yaml + # + singleNode: true + resources: + requests: + cpu: 1 + memory: 1Gi + persistence: + size: 10Gi + extraEnvs: + - name: plugins.security.ssl.http.enabled + value: "false" + envFrom: + - secretRef: + # -- If using a pre-created secret, make sure to change secret name to match opensearch.credentials.existingSecretRef.name + # + name: opensearch-initial-password diff --git a/src/test/config/jira/values.yaml b/src/test/config/jira/values.yaml index 3072d4c16..356ee96a3 100644 --- a/src/test/config/jira/values.yaml +++ b/src/test/config/jira/values.yaml @@ -17,5 +17,11 @@ database: credentials: secretName: jira-database-credentials +opensearch: + enabled: true + extraEnvs: + - name: plugins.security.disabled + value: "true" + monitoring: exposeJmxMetrics: true diff --git a/src/test/java/test/AdditionalConfigPropertiesTest.java b/src/test/java/test/AdditionalConfigPropertiesTest.java new file mode 100644 index 000000000..7735af6b1 --- /dev/null +++ b/src/test/java/test/AdditionalConfigPropertiesTest.java @@ -0,0 +1,72 @@ +package test; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import test.helm.Helm; +import test.model.Product; + +import java.util.Map; + +import static test.jackson.JsonNodeAssert.assertThat; + +class AdditionalConfigPropertiesTest { + + private static final Product JIRA = Product.jira; + + private Helm helm; + + @BeforeEach + void initHelm(TestInfo testInfo) { + helm = new Helm(testInfo); + } + + @Test + void no_additional_config_properties_by_default() throws Exception { + final var resources = helm.captureKubeResourcesFromHelmChart(JIRA, Map.of()); + + final var statefulSet = resources.getStatefulSet(JIRA.getHelmReleaseName()); + final var env = statefulSet.getContainer().getEnv(); + env.assertDoesNotHaveAnyOf( + "ADDITIONAL_JIRA_CONFIG_HELM_000", + "ADDITIONAL_JIRA_CONFIG_HELM_000__EXPAND_ENV" + ); + } + + @Test + void additional_config_properties_are_set() throws Exception { + final var resources = helm.captureKubeResourcesFromHelmChart(JIRA, Map.of( + "jira.additionalConfigProperties[0]", "jira.websudo.is.disabled=true", + "jira.additionalConfigProperties[1]", "jira.lf.top.bgcolour=#003366" + )); + + final var statefulSet = resources.getStatefulSet(JIRA.getHelmReleaseName()); + final var env = statefulSet.getContainer().getEnv(); + env.assertHasValue("ADDITIONAL_JIRA_CONFIG_HELM_000", "jira.websudo.is.disabled=true"); + env.assertHasValue("ADDITIONAL_JIRA_CONFIG_HELM_001", "jira.lf.top.bgcolour=#003366"); + } + + @Test + void additional_config_properties_expand_env_are_set() throws Exception { + final var resources = helm.captureKubeResourcesFromHelmChart(JIRA, Map.of( + "jira.additionalConfigPropertiesExpandEnv[0]", "opensearch.password={OPENSEARCH_INITIAL_ADMIN_PASSWORD}" + )); + + final var statefulSet = resources.getStatefulSet(JIRA.getHelmReleaseName()); + final var env = statefulSet.getContainer().getEnv(); + env.assertHasValue("ADDITIONAL_JIRA_CONFIG_HELM_000__EXPAND_ENV", "opensearch.password={OPENSEARCH_INITIAL_ADMIN_PASSWORD}"); + } + + @Test + void both_config_properties_and_expand_env_are_set() throws Exception { + final var resources = helm.captureKubeResourcesFromHelmChart(JIRA, Map.of( + "jira.additionalConfigProperties[0]", "some.property=value1", + "jira.additionalConfigPropertiesExpandEnv[0]", "secret.property={MY_SECRET}" + )); + + final var statefulSet = resources.getStatefulSet(JIRA.getHelmReleaseName()); + final var env = statefulSet.getContainer().getEnv(); + env.assertHasValue("ADDITIONAL_JIRA_CONFIG_HELM_000", "some.property=value1"); + env.assertHasValue("ADDITIONAL_JIRA_CONFIG_HELM_000__EXPAND_ENV", "secret.property={MY_SECRET}"); + } +} diff --git a/src/test/java/test/JiraOpenSearchTest.java b/src/test/java/test/JiraOpenSearchTest.java new file mode 100644 index 000000000..18b745bfd --- /dev/null +++ b/src/test/java/test/JiraOpenSearchTest.java @@ -0,0 +1,79 @@ +package test; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import test.helm.Helm; +import test.model.Product; + +import java.util.Base64; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static test.jackson.JsonNodeAssert.assertThat; +import static test.model.Kind.Secret; + +class JiraOpenSearchTest { + + private static final Product JIRA = Product.jira; + + private Helm helm; + + @BeforeEach + void initHelm(TestInfo testInfo) { + helm = new Helm(testInfo); + } + + @Test + void opensearch_statefulset_is_created_when_enabled() throws Exception { + final var resources = helm.captureKubeResourcesFromHelmChart(JIRA, Map.of( + "opensearch.enabled", "true" + )); + final var statefulSet = resources.getStatefulSet("opensearch-cluster-master"); + assertThat(statefulSet.getSpec()).isNotNull(); + } + + @Test + void opensearch_secret_contains_valid_base64_password() throws Exception { + final var resources = helm.captureKubeResourcesFromHelmChart(JIRA, Map.of( + "opensearch.enabled", "true" + )); + final var secret = resources.get(Secret, "opensearch-initial-password"); + JsonNode password = secret.getConfigMapData().path("OPENSEARCH_INITIAL_ADMIN_PASSWORD"); + assertThat(password).isNotNull(); + assertDoesNotThrow(() -> { + Base64.getDecoder().decode(password.asText()); + }, "Password should be a valid Base64 encoded string"); + byte[] decodedPassword = Base64.getDecoder().decode(password.asText()); + assertEquals(40, decodedPassword.length, "The decoded password should have a length of 40 bytes."); + } + + @Test + void opensearch_env_vars_are_set_with_default_credentials() throws Exception { + final var resources = helm.captureKubeResourcesFromHelmChart(JIRA, Map.of( + "opensearch.enabled", "true" + )); + + final var statefulSet = resources.getStatefulSet(JIRA.getHelmReleaseName()); + final var env = statefulSet.getContainer().getEnv(); + env.assertHasValue("ADDITIONAL_JIRA_CONFIG_SEARCH_PLATFORM", "search.platform=opensearch"); + env.assertHasValue("ADDITIONAL_JIRA_CONFIG_SEARCH_URL", "opensearch.http.url=http://opensearch-cluster-master:9200"); + env.assertHasValue("ADDITIONAL_JIRA_CONFIG_SEARCH_USERNAME", "opensearch.username=admin"); + env.assertHasSecretRef("OPENSEARCH_ADMIN_PASSWORD", "opensearch-initial-password", "OPENSEARCH_INITIAL_ADMIN_PASSWORD"); + env.assertHasValue("ADDITIONAL_JIRA_CONFIG_SEARCH_PASSWORD__EXPAND_ENV", "opensearch.password={OPENSEARCH_ADMIN_PASSWORD}"); + } + + @Test + void opensearch_env_vars_use_existing_secret_when_configured() throws Exception { + final var resources = helm.captureKubeResourcesFromHelmChart(JIRA, Map.of( + "opensearch.enabled", "true", + "opensearch.credentials.existingSecretRef.name", "my-opensearch-secret" + )); + + final var statefulSet = resources.getStatefulSet(JIRA.getHelmReleaseName()); + final var env = statefulSet.getContainer().getEnv(); + env.assertHasSecretRef("OPENSEARCH_ADMIN_PASSWORD", "my-opensearch-secret", "OPENSEARCH_INITIAL_ADMIN_PASSWORD"); + } +} diff --git a/src/test/resources/expected_helm_output/jira/output.yaml b/src/test/resources/expected_helm_output/jira/output.yaml index 2aff8156c..1984c3744 100644 --- a/src/test/resources/expected_helm_output/jira/output.yaml +++ b/src/test/resources/expected_helm_output/jira/output.yaml @@ -144,6 +144,8 @@ data: securityContext: {} secretList: [] secretName: null + additionalConfigProperties: [] + additionalConfigPropertiesExpandEnv: [] additionalEnvironmentVariables: [] additionalJvmArgs: [] additionalLibraries: [] @@ -285,6 +287,25 @@ data: scrapeIntervalSeconds: 30 scrapeTimeoutSeconds: 20 nodeSelector: {} + opensearch: + credentials: + createSecret: true + existingSecretRef: + name: null + enabled: false + envFrom: + - secretRef: + name: opensearch-initial-password + extraEnvs: + - name: plugins.security.ssl.http.enabled + value: "false" + persistence: + size: 10Gi + resources: + requests: + cpu: 1 + memory: 1Gi + singleNode: true openshift: runWithRestrictedSCC: false ordinals: @@ -425,7 +446,6 @@ spec: template: metadata: annotations: - checksum/config-jvm: 5d632191c42871616de74827f73c9c72e383ec5c85f91e28f69c079802792851 labels: helm.sh/chart: jira-2.0.9 app.kubernetes.io/name: jira diff --git a/src/test/scripts/helm_install.sh b/src/test/scripts/helm_install.sh index 34fa46585..e0a58a447 100755 --- a/src/test/scripts/helm_install.sh +++ b/src/test/scripts/helm_install.sh @@ -249,7 +249,7 @@ package_functest_helm_chart() { for ((NODE = 0; NODE < ${TARGET_REPLICA_COUNT:-0}; NODE += 1)); do backdoor_services+="- ${PRODUCT_RELEASE_NAME}-${NODE}${NEWLINE}" done - if [[ "$PRODUCT_NAME" == "bitbucket" ]]; then + if [[ "$PRODUCT_NAME" == "bitbucket" || "$PRODUCT_NAME" == "jira" ]]; then echo "OpenSearch is being deployed, adding a backdoor" backdoor_services+="- opensearch-cluster-master-0${NEWLINE}" fi diff --git a/src/test/scripts/kind/deploy_app.sh b/src/test/scripts/kind/deploy_app.sh index 4de15d4a2..c0e95ea99 100755 --- a/src/test/scripts/kind/deploy_app.sh +++ b/src/test/scripts/kind/deploy_app.sh @@ -479,6 +479,87 @@ verify_openshift_analytics() { fi } +verify_opensearch() { + # Only Jira and Bitbucket use OpenSearch in KinD tests + if [ "${DC_APP}" != "jira" ] && [ "${DC_APP}" != "bitbucket" ]; then + echo "[INFO]: OpenSearch verification is not applicable for ${DC_APP}, skipping" + return 0 + fi + + # OpenSearch is disabled on MicroShift/OpenShift + if [ -n "${OPENSHIFT_VALUES:-}" ]; then + echo "[INFO]: OpenSearch is disabled on OpenShift, skipping verification" + return 0 + fi + + echo "[INFO]: Verifying OpenSearch is being used by ${DC_APP}" + + OS_POD="opensearch-cluster-master-0" + RETRIES=12 + SLEEP_INTERVAL=5 + + # Retrieve OpenSearch admin password from the Kubernetes secret + OS_PASSWORD=$(kubectl get secret opensearch-initial-password -n atlassian -o jsonpath='{.data.OPENSEARCH_INITIAL_ADMIN_PASSWORD}' | base64 -d) + if [ -z "${OS_PASSWORD}" ]; then + echo "[ERROR]: Failed to retrieve OpenSearch admin password from secret 'opensearch-initial-password'" + exit 1 + fi + OS_CURL_AUTH="admin:${OS_PASSWORD}" + + # First, wait for the OpenSearch pod to be ready + echo "[INFO]: Waiting for OpenSearch pod to be ready" + kubectl wait --for=condition=ready pod/${OS_POD} -n atlassian --timeout=300s || { + echo "[ERROR]: OpenSearch pod ${OS_POD} did not become ready" + kubectl describe pod/${OS_POD} -n atlassian || true + exit 1 + } + + for i in $(seq 1 ${RETRIES}); do + INDICES=$(kubectl exec -n atlassian ${OS_POD} -- \ + curl -s -u "${OS_CURL_AUTH}" http://localhost:9200/_cat/indices?format=json 2>/dev/null) || true + + if [ -z "${INDICES}" ] || [ "${INDICES}" = "null" ]; then + echo "[INFO]: OpenSearch returned empty response, retrying... (${i}/${RETRIES})" + sleep ${SLEEP_INTERVAL} + continue + fi + + if [ "${DC_APP}" = "bitbucket" ]; then + # Bitbucket creates a 'bitbucket-index-version' index with exactly 1 document + DOC_COUNT=$(echo "${INDICES}" | jq -r '[.[] | select(.index == "bitbucket-index-version")] | .[0] | .["docs.count"] // empty' 2>/dev/null) || true + if [ -n "${DOC_COUNT}" ] && [ "${DOC_COUNT}" = "1" ]; then + echo "[INFO]: OpenSearch verification passed for Bitbucket: bitbucket-index-version has docs.count=${DOC_COUNT}" + return 0 + fi + echo "[INFO]: Waiting for Bitbucket to create index in OpenSearch... (${i}/${RETRIES})" + + elif [ "${DC_APP}" = "jira" ]; then + # Jira creates a 'jira-issues-*' index on startup; on a fresh instance it will have 0 documents + INDEX_EXISTS=$(echo "${INDICES}" | jq -r '[.[] | select(.index | test("^jira-issues-"))] | length' 2>/dev/null) || true + if [ -n "${INDEX_EXISTS}" ] && [ "${INDEX_EXISTS}" != "0" ]; then + DOC_COUNT=$(echo "${INDICES}" | jq -r '[.[] | select(.index | test("^jira-issues-"))] | .[0] | .["docs.count"] // "0"' 2>/dev/null) || true + echo "[INFO]: OpenSearch verification passed for Jira: jira-issues index exists (docs.count=${DOC_COUNT})" + return 0 + fi + echo "[INFO]: Waiting for Jira to create index in OpenSearch... (${i}/${RETRIES})" + fi + + sleep ${SLEEP_INTERVAL} + done + + echo "[ERROR]: OpenSearch verification failed for ${DC_APP} after $((RETRIES * SLEEP_INTERVAL)) seconds" + echo "[DEBUG]: OpenSearch indices:" + kubectl exec -n atlassian ${OS_POD} -- curl -s -u "${OS_CURL_AUTH}" http://localhost:9200/_cat/indices?format=json 2>/dev/null | jq . || true + echo "[DEBUG]: OpenSearch cluster health:" + kubectl exec -n atlassian ${OS_POD} -- curl -s -u "${OS_CURL_AUTH}" http://localhost:9200/_cat/health 2>/dev/null || true + echo "[DEBUG]: ${DC_APP} pod logs (last 200 lines):" + for pod in $(kubectl get pods -n atlassian -l app.kubernetes.io/name=${DC_APP} --no-headers -o custom-columns=":metadata.name" 2>/dev/null); do + echo "--- Logs from ${pod} ---" + kubectl logs "${pod}" -n atlassian --tail=200 2>/dev/null || true + done + exit 1 +} + # create 2 NodePort services to expose each DC pod, required for functional tests # where communication between nodes and cache replication is tested create_backdoor_services() {