diff --git a/docs/docs/containers/JIRA.md b/docs/docs/containers/JIRA.md index 632b83a83..213b8fc0d 100644 --- a/docs/docs/containers/JIRA.md +++ b/docs/docs/containers/JIRA.md @@ -219,6 +219,26 @@ information, please refer to Override the default AWS API endpoint with a custom one (optional). +### OpenSearch configuration + +Starting with Jira 11.0, you can configure Jira to use OpenSearch as the search platform. + +* `ATL_SEARCH_PLATFORM` + + The search platform to use. Set to `opensearch` if you want to use OpenSearch as the search platform. + +* `ATL_OPENSEARCH_HTTP_URL` + + HTTP(S) URL of the OpenSearch cluster, or multiple URLs separated by commas. + +* `ATL_OPENSEARCH_USERNAME` + + Username for the OpenSearch cluster. + +* `ATL_OPENSEARCH_PASSWORD` + + Password for the OpenSearch cluster. + ### 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). 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..1dee24569 --- /dev/null +++ b/docs/docs/examples/jira/JIRA_OPENSEARCH.md @@ -0,0 +1,54 @@ +# Configuring OpenSearch for Jira + +!!!info "Jira and Helm chart version" + OpenSearch is supported in Jira 11.0.0 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 `ATL_SEARCH_PLATFORM=opensearch`, `ATL_OPENSEARCH_HTTP_URL=http://opensearch-cluster-master:9200`, `ATL_OPENSEARCH_USERNAME=admin` and `ATL_OPENSEARCH_PASSWORD` environment variables on the Jira container + +## 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`. 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..86c1a65c7 100644 --- a/src/main/charts/jira/templates/_helpers.tpl +++ b/src/main/charts/jira/templates/_helpers.tpl @@ -545,3 +545,41 @@ 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: ATL_SEARCH_PLATFORM + value: opensearch +- name: ATL_OPENSEARCH_HTTP_URL + value: http://opensearch-cluster-master:9200 +- name: ATL_OPENSEARCH_USERNAME + value: admin +- name: ATL_OPENSEARCH_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.opensearch.credentials.existingSecretRef.name | default "opensearch-initial-password" }} + key: OPENSEARCH_INITIAL_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..94f3b2ff2 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,7 @@ spec: - name: CATALINA_OPTS value: {{ include "common.jmx.javaagent" . | replace "\n" "" | quote }} {{- end }} + {{- include "opensearch.env.vars" . | 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..9a9b7da48 100644 --- a/src/main/charts/jira/values.yaml +++ b/src/main/charts/jira/values.yaml @@ -1347,3 +1347,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/JiraOpenSearchTest.java b/src/test/java/test/JiraOpenSearchTest.java new file mode 100644 index 000000000..abb28b593 --- /dev/null +++ b/src/test/java/test/JiraOpenSearchTest.java @@ -0,0 +1,78 @@ +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("ATL_SEARCH_PLATFORM", "opensearch"); + env.assertHasValue("ATL_OPENSEARCH_HTTP_URL", "http://opensearch-cluster-master:9200"); + env.assertHasValue("ATL_OPENSEARCH_USERNAME", "admin"); + env.assertHasSecretRef("ATL_OPENSEARCH_PASSWORD", "opensearch-initial-password", "OPENSEARCH_INITIAL_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("ATL_OPENSEARCH_PASSWORD", "my-opensearch-secret", "OPENSEARCH_INITIAL_ADMIN_PASSWORD"); + } +} diff --git a/src/test/java/test/postinstall/OpenSearchInstallTest.java b/src/test/java/test/postinstall/OpenSearchInstallTest.java index 0366b779f..8977c9992 100644 --- a/src/test/java/test/postinstall/OpenSearchInstallTest.java +++ b/src/test/java/test/postinstall/OpenSearchInstallTest.java @@ -29,7 +29,7 @@ @EnabledIf("isOSDeployed") class OpenSearchInstallTest { static boolean isOSDeployed() { - return productIs(Product.bitbucket); + return productIs(Product.bitbucket) || productIs(Product.jira); } private static KubeClient client; @@ -64,8 +64,13 @@ void openSearchBeingUsed() { // If this changes an alternative would be to use the fabric8 client ExecWatch/ExecListener to // invoke curl from a pod. final var indexURL = osIngressBase + "/_cat/indices?format=json"; - when().get(indexURL).then() - .body("findAll { it.index == 'bitbucket-index-version' }[0]", hasEntry("docs.count", "1")); + if (productIs(Product.bitbucket)) { + when().get(indexURL).then() + .body("findAll { it.index == 'bitbucket-index-version' }[0]", hasEntry("docs.count", "1")); + } else if (productIs(Product.jira)) { + when().get(indexURL).then() + .body("findAll { it.index =~ /jira.*/ }.size()", greaterThan(0)); + } } catch (Exception e) { retries--; try { diff --git a/src/test/resources/expected_helm_output/jira/output.yaml b/src/test/resources/expected_helm_output/jira/output.yaml index 2aff8156c..053bdfd37 100644 --- a/src/test/resources/expected_helm_output/jira/output.yaml +++ b/src/test/resources/expected_helm_output/jira/output.yaml @@ -285,6 +285,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: 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