diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..32a8b72d3e49972f0e0c11aec629c1262ed8b08c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +ARG BASE_REGISTRY=registry1.dso.mil +ARG BASE_IMAGE=ironbank/redhat/ubi/ubi8 +ARG BASE_TAG=8.3 +FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} + +ENV RUN_USER jira +ENV RUN_GROUP jira +ENV RUN_UID 2001 +ENV RUN_GID 2001 + +# https://confluence.atlassian.com/display/JSERVERM/Important+directories+and+files +ENV JIRA_HOME /var/atlassian/application-data/jira +ENV JIRA_INSTALL_DIR /opt/atlassian/jira + +WORKDIR $JIRA_HOME + +# Expose HTTP port +EXPOSE 8080 +# JIRA Cluster Sync Port +EXPOSE 40001 + + +CMD ["/entrypoint.py"] +ENTRYPOINT ["/usr/bin/tini", "--"] + + +COPY dumb-init-1.2.2-6.el8.x86_64.rpm /opt/ + +RUN dnf -y update && dnf -y upgrade && \ + dnf -y install python3 python3-jinja2 && \ + rpm -Uvh /opt/dumb-init-1.2.2-6.el8.x86_64.rpm + +ARG JIRA_VERSION=8.13.3 +ARG ARTEFACT_NAME=atlassian-jira-software +COPY atlassian-jira-software-${JIRA_VERSION}.tar.gz /opt/ + +RUN groupadd --gid ${RUN_GID} ${RUN_GROUP} \ + && useradd --uid ${RUN_UID} --gid ${RUN_GID} --home-dir ${JIRA_HOME} --shell /bin/bash ${RUN_USER} \ + && echo PATH=$PATH > /etc/environment \ + \ + && mkdir -p ${JIRA_INSTALL_DIR} \ + && tar -xzf /opt/atlassian-jira-software-${JIRA_VERSION}.tar.gz --strip-components 1 -C /opt/atlassian/jira/ \ + && chmod -R "u=rwX,g=rX,o=rX" ${JIRA_INSTALL_DIR}/ \ + && chown -R root. ${JIRA_INSTALL_DIR}/ \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${JIRA_INSTALL_DIR}/logs \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${JIRA_INSTALL_DIR}/temp \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${JIRA_INSTALL_DIR}/work \ + \ + && sed -i -e 's/^JVM_SUPPORT_RECOMMENDED_ARGS=""$/: \${JVM_SUPPORT_RECOMMENDED_ARGS:=""}/g' ${JIRA_INSTALL_DIR}/bin/setenv.sh \ + && sed -i -e 's/^JVM_\(.*\)_MEMORY="\(.*\)"$/: \${JVM_\1_MEMORY:=\2}/g' ${JIRA_INSTALL_DIR}/bin/setenv.sh \ + && sed -i -e 's/-XX:ReservedCodeCacheSize=\([0-9]\+[kmg]\)/-XX:ReservedCodeCacheSize=${JVM_RESERVED_CODE_CACHE_SIZE:=\1}/g' ${JIRA_INSTALL_DIR}/bin/setenv.sh \ + \ + && touch /etc/container_id \ + && chown ${RUN_USER}:${RUN_GROUP} /etc/container_id \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${JIRA_HOME} + +VOLUME ["${JIRA_HOME}"] # Must be declared after setting perms + +COPY scripts/entrypoint.py \ + scripts/entrypoint_helpers.py / +COPY scripts/shared-components/support /opt/atlassian/support +COPY config/* /opt/atlassian/etc/ + +#### Clean up #### +RUN rm -rf /opt/dumb-init-1.2.2-6.el8.x86_64.rpm && dnf clean all && rm -rf /opt/${ARTEFACT_NAME}-${JIRA_VERSION}.tar.gz + +# +# HEALTHCHECK +# +HEALTHCHECK --start-period=5m --interval=3m --timeout=3s \ + CMD curl -f http://localhost:8080/ || exit 1 + +# +# RUN +# +USER ${RUN_USER} +ENTRYPOINT ["/entrypoint.py"] +CMD ["${JIRA_INSTALL_DIR}/bin/catalina.sh", "run"] + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..7f33e9e8d0a5b7b46d9c6b070bb81fbfbda3a3a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +A commercial license will be required to run your Jira instance. + +A 30 Day Unlimited Trial License for Jira and any associated add-ons / apps in +the marketplace may be acquired by contacting licensing@ascendintegrated.com. + +Purchase of a license will be facilitated by contacting +licensing@ascendintegrated.com. diff --git a/README.md b/README.md index 5dc6fa6db4361c22da2f35edf0544d83ba6001e2..c7eb604a75e7682df5dcf6544cd5097ae1fb26fe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,34 @@ -# +# JIRA Data Center +## jira-node -Project template for all Iron Bank container repositories. \ No newline at end of file +### Build and run image + +**1.** Build image + + docker build -t . + +**2.** Run image + + docker run -t --name jira-cluster-node --net= -v :/jira-shared-home -e JIRA_NODE_NAME= + +### Recommended resource requirements + +**1.** Min/max cpu + + 2/- + +**2.** Min/max memory + + 2g/- + +**3.** Storage min/max/limits + + 10gb/-/- + +**4.** How many storage volumes the application needs + + 1 + +**5.** Max number of containers + + n/a diff --git a/config/cluster.properties.j2 b/config/cluster.properties.j2 new file mode 100644 index 0000000000000000000000000000000000000000..50d59cff2717a4d12a34fcbaaf9223d371b25ee3 --- /dev/null +++ b/config/cluster.properties.j2 @@ -0,0 +1,11 @@ +jira.node.id={{ jira_node_id | default(container_id) | default(uuid) }} +jira.shared.home={{ jira_shared_home | default(jira_home+'/shared') }} +{% if ehcache_peer_discovery is defined %}ehcache.peer.discovery={{ ehcache_peer_discovery }}{% else %}# ehcache.peer.discovery={% endif %} +{% if ehcache_listener_hostname is defined %}ehcache.listener.hostName={{ ehcache_listener_hostname }}{% else %}# ehcache.listener.hostName={% endif %} +{% if ehcache_object_port is defined %}ehcache.object.port={{ ehcache_object_port }}{% else %}# ehcache.object.port={% endif %} +{% if ehcache_listener_port is defined %}ehcache.listener.port={{ ehcache_listener_port }}{% else %}# ehcache.listener.port={% endif %} +{% if ehcache_listener_sockettimeoutmillis is defined %}ehcache.listener.socketTimeoutMillis={{ ehcache_listener_sockettimeoutmillis }}{% else %}# ehcache.listener.socketTimeoutMillis={% endif %} +{% if ehcache_multicast_address is defined %}ehcache.multicast.address={{ ehcache_multicast_address }}{% else %}# ehcache.multicast.address={% endif %} +{% if ehcache_multicast_port is defined %}ehcache.multicast.port={{ ehcache_multicast_port }}{% else %}# ehcache.multicast.port={% endif %} +{% if ehcache_multicast_timetolive is defined %}ehcache.multicast.timeToLive={{ ehcache_multicast_timetolive }}{% else %}# ehcache.multicast.timeToLive={% endif %} +{% if ehcache_multicast_hostname is defined %}ehcache.multicast.hostName={{ ehcache_multicast_hostname }}{% else %}# ehcache.multicast.hostName={% endif %} diff --git a/config/container_id.j2 b/config/container_id.j2 new file mode 100644 index 0000000000000000000000000000000000000000..79407dab9638cd10a2754941f2401f634abd7cd2 --- /dev/null +++ b/config/container_id.j2 @@ -0,0 +1 @@ +{{ container_id | default(local_container_id) | default(uuid) }} diff --git a/config/dbconfig.xml.j2 b/config/dbconfig.xml.j2 new file mode 100644 index 0000000000000000000000000000000000000000..f77af1b1f410b14c2e4dcbc09a496ca6b675d898 --- /dev/null +++ b/config/dbconfig.xml.j2 @@ -0,0 +1,34 @@ + + + + defaultDS + default + {% set schema_names = { + "mssql": "dbo", + "mysql": "public", + "oracle10g": "", + "postgres72": "public" + } %} + {{ atl_db_schema_name | default(schema_names.get(atl_db_type, '')) }} + {{ atl_db_type }} + + {{ atl_jdbc_url }} + {{ atl_jdbc_user }} + {{ atl_jdbc_password }} + {{ atl_db_driver }} + + {{ atl_db_poolminsize | default('20') }} + {{ atl_db_poolmaxsize | default('100') }} + {{ atl_db_minidle | default('10') }} + {{ atl_db_maxidle | default('20') }} + + {{ atl_db_maxwaitmillis | default('30000') }} + {{ atl_db_validationquery | default('select 1') }} + {{ atl_db_timebetweenevictionrunsmillis | default('30000') }} + {{ atl_db_minevictableidletimemillis | default('5000') }} + {{ atl_db_removeabandoned | default('true') }} + {{ atl_db_removeabandonedtimeout | default('300') }} + {{ atl_db_testwhileidle | default('true') }} + {{ atl_db_testonborrow | default('false') }} + + diff --git a/config/seraph-config.xml.j2 b/config/seraph-config.xml.j2 new file mode 100644 index 0000000000000000000000000000000000000000..861ab3135d204f1a1637e5fb09d7e2f6f59dc5a1 --- /dev/null +++ b/config/seraph-config.xml.j2 @@ -0,0 +1,124 @@ + + + + + login.url + /login.jsp?permissionViolation=true&os_destination=${originalurl}&page_caps=${pageCaps}&user_role=${userRole} + + + + + link.login.url + /login.jsp?os_destination=${originalurl} + + + + + + logout.url + /secure/Logout!default.jspa + + + + + login.forward.path + /secure/XsrfErrorAction.jspa + + + + original.url.key + os_security_originalurl + + + login.cookie.key + seraph.rememberme.cookie + + + + autologin.cookie.age + {{ atl_autologin_cookie_age | default('1209600') }} + + + + authentication.type + os_authType + + + + + invalidate.session.on.login + true + + + invalidate.session.exclude.list + ASESSIONID,jira.websudo.timestamp,jira.user.project.admin + + + + + + + + + + + + + + + + + + action.extension + jspa + + + + + + + + + + + + + + diff --git a/config/server.xml.j2 b/config/server.xml.j2 new file mode 100644 index 0000000000000000000000000000000000000000..566ad8d4a8a4a0fefdc7c73dc4e7f121f71009b2 --- /dev/null +++ b/config/server.xml.j2 @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hardening_manifest.yaml b/hardening_manifest.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ec7b18a6e90f2caf966da04692b517a5c08f76c6 --- /dev/null +++ b/hardening_manifest.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: v1 + +# The repository name in registry1, excluding /ironbank/ +name: "atlassian/jira-data-center/jira-node" + +# List of tags to push for the repository in registry1 +# The most specific version should be the first tag and will be shown +# on ironbank.dsop.io +tags: +- "8.13.3" +- "latest" + +# Build args passed to Dockerfile ARGs +args: + BASE_IMAGE: "redhat/ubi/ubi8" + BASE_TAG: "8.3" + +# Docker image labels +labels: + org.opencontainers.image.title: "jira-node" + org.opencontainers.image.description: "Jira is a project and task management solution built for business teams." + org.opencontainers.image.licenses: "proprietary" + org.opencontainers.image.url: "https://hub.docker.com/r/atlassian/jira-core" + org.opencontainers.image.vendor: "Atlassian" + org.opencontainers.image.version: "8.13.3" + mil.dso.ironbank.image.keywords: "jira,atlassian,workflow,ticketing,management" + mil.dso.ironbank.image.type: "commercial" + mil.dso.ironbank.product.name: "atlassian" + +# List of resources to make available to the offline build context +resources: +- filename: dumb-init-1.2.2-6.el8.x86_64.rpm + url: https://cbs.centos.org/kojifiles/packages/dumb-init/1.2.2/6.el8/x86_64/dumb-init-1.2.2-6.el8.x86_64.rpm + validation: + type: sha512 + value: bcfabdb039ce06a49d39d4bcc1d70fa38ee56ed50edc06019648a14ec898ad5afa1ec30dbff2da166c8f5557316b2b3659523787d34cb81223cf3a373242fa3f +- filename: atlassian-jira-software-8.13.3.tar.gz + url: https://product-downloads.atlassian.com/software/jira/downloads/atlassian-jira-software-8.13.3.tar.gz + validation: + type: sha512 + value: cd387abb44c8d58333dcd9e567c02741f6f32dfe6c1ee9106f9dea67a8772d749a34a09e939f8f9820e389d30c1c4e8de9c0729d4355b810475764693df4c65a + +# List of project maintainers +maintainers: +- email: "support@ascendintegrated.com" + name: "jweatherford@oteemo.com" + username: "jweatherford" + cht_member: true +- name: "James Hunt" + username: "jhunt" + email: "jhunt@oteemo.com" diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9412ba658695986982600303263efb879cf7e6e3 --- /dev/null +++ b/helm/README.md @@ -0,0 +1,13 @@ +# jira-cluster-node helm chart setup +## jira-cluster-node + +**1.** Run helm install for database if needed + + helm install jira-cluster-db jira-cluster-db + +**2.** Run helm install for atlassian product + + helm install --set jira_node_name= jira-cluster-node + +# Notes +Image repository in values.yaml should be replaced with actual repository. diff --git a/helm/jira-cluster-db/.helmignore b/helm/jira-cluster-db/.helmignore new file mode 100644 index 0000000000000000000000000000000000000000..50af0317254197a5a019f4ac2f8ecc223f93f5a7 --- /dev/null +++ b/helm/jira-cluster-db/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/jira-cluster-db/Chart.yaml b/helm/jira-cluster-db/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..08d401cfcdd5cb8d60b271e4d7de30c6cf9c07a6 --- /dev/null +++ b/helm/jira-cluster-db/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +version: 0.1.0 +appVersion: 9.6 +description: PostgreSQL is an open-source object-relational database management system (ORDBMS) emphasizing extensibility and technical standards compliance. +name: jira-cluster-db +keywords: + - postgresql + - postgres + - database +home: https://www.postgresql.org/ +icon: https://en.wikipedia.org/wiki/PostgreSQL#/media/File:Postgresql_elephant.svg +maintainers: + - name: Ascend Integrated diff --git a/helm/jira-cluster-db/configs/README.md b/helm/jira-cluster-db/configs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1813a2feaaf1a77b1b7cb37317c47e9b619d3a21 --- /dev/null +++ b/helm/jira-cluster-db/configs/README.md @@ -0,0 +1 @@ +Copy here your postgresql.conf and/or pg_hba.conf files to use it as a config map. diff --git a/helm/jira-cluster-db/templates/_helpers.tpl b/helm/jira-cluster-db/templates/_helpers.tpl new file mode 100644 index 0000000000000000000000000000000000000000..571c399945e414f6c92dbc9ae906f6bc443e072a --- /dev/null +++ b/helm/jira-cluster-db/templates/_helpers.tpl @@ -0,0 +1,31 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "postgresql.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "postgresql.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "postgresql.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/helm/jira-cluster-db/templates/configmap.yaml b/helm/jira-cluster-db/templates/configmap.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b4ac49d0b7b18f24dfc953fad6f894ae94f94e2d --- /dev/null +++ b/helm/jira-cluster-db/templates/configmap.yaml @@ -0,0 +1,27 @@ +--- +{{- if and (or (.Files.Glob "configs/postgresql.conf") (.Files.Glob "configs/pg_hba.conf") .Values.postgresql.config .Values.postgresql.pghba) (not .Values.postgresql.configMap) }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "postgresql.fullname" . }}-configuration + labels: + app: {{ template "postgresql.name" . }} + chart: {{ template "postgresql.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +data: +{{- if (.Files.Glob "configs/postgresql.conf") }} +{{ (.Files.Glob "configs/postgresql.conf").AsConfig | indent 2 }} +{{- else if .Values.postgresql.config }} + postgresql.conf: | +{{- range $key, $value := default dict .Values.postgresql.config }} + {{ $key | snakecase }}={{ $value }} +{{- end }} +{{- end }} +{{- if (.Files.Glob "configs/pg_hba.conf") }} +{{ (.Files.Glob "configs/pg_hba.conf").AsConfig | indent 2 }} +{{- else if .Values.postgresql.pghba }} + pg_hba.conf: | +{{ .Values.postgresql.pghba | indent 4 }} +{{- end }} +{{- end }} diff --git a/helm/jira-cluster-db/templates/service.yaml b/helm/jira-cluster-db/templates/service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..36425827e2f846c4416d153e70e18ae99a6e6bed --- /dev/null +++ b/helm/jira-cluster-db/templates/service.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ template "postgresql.fullname" . }} + labels: + app: {{ template "postgresql.name" . }} + chart: {{ template "postgresql.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +{{- with .Values.service.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: + type: {{ .Values.service.type }} + {{- if and .Values.service.loadBalancerIP (eq .Values.service.type "LoadBalancer") }} + loadBalancerIP: {{ .Values.service.loadBalancerIP }} + {{- end }} + {{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{ with .Values.service.loadBalancerSourceRanges }} +{{ toYaml . | indent 4 }} +{{- end }} + {{- end }} + {{- if and (eq .Values.service.type "ClusterIP") .Values.service.clusterIP }} + clusterIP: {{ .Values.service.clusterIP }} + {{- end }} + ports: + - name: postgresql + port: {{ .Values.postgresql.port }} + targetPort: postgresql + selector: + app: {{ template "postgresql.name" . }} + release: {{ .Release.Name | quote }} diff --git a/helm/jira-cluster-db/templates/statefulset.yaml b/helm/jira-cluster-db/templates/statefulset.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f176c20c7caea8ee7fabd75343df10f7685c2e78 --- /dev/null +++ b/helm/jira-cluster-db/templates/statefulset.yaml @@ -0,0 +1,148 @@ +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ template "postgresql.fullname" . }} + labels: + app: {{ include "postgresql.name" . }} + chart: {{ template "postgresql.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +spec: + serviceName: {{ template "postgresql.fullname" . }}-headless + replicas: 1 + selector: + matchLabels: + app: {{ template "postgresql.name" . }} + release: {{ .Release.Name | quote }} + template: + metadata: + name: {{ template "postgresql.fullname" . }} + labels: + app: {{ include "postgresql.name" . | quote }} + chart: {{ template "postgresql.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} + spec: + {{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }} + initContainers: + - name: init-chmod-data + image: "{{ .Values.volumePermissions.image.repository }}:{{ .Values.volumePermissions.image.tag }}" + imagePullPolicy: "{{ .Values.volumePermissions.image.pullPolicy }}" + resources: +{{ toYaml .Values.resources | indent 10 }} + command: + - sh + - -c + - | + mkdir -p {{ .Values.persistence.mountPath }}/data + chmod 700 {{ .Values.persistence.mountPath }}/data + find {{ .Values.persistence.mountPath }} -mindepth 1 -maxdepth 1 -not -name ".snapshot" -not -name "lost+found" | \ + xargs chown -R {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.fsGroup }} + securityContext: + runAsUser: {{ .Values.volumePermissions.securityContext.runAsUser }} + volumeMounts: + - name: data + mountPath: {{ .Values.persistence.mountPath }} + subPath: {{ .Values.persistence.subPath }} + {{- end }} + containers: + - name: {{ template "postgresql.fullname" . }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + {{- if and .Values.postgresql.pghba .Values.postgresql.config}} + args: ["-c", "config_file={{ .Values.persistence.mountPath }}/conf/postgresql.conf", "-c", "hba_file={{ .Values.persistence.mountPath }}/conf/pg_hba.conf"] + {{- end }} + {{- if and (not .Values.postgresql.pghba) .Values.postgresql.config}} + args: ["-c", "config_file={{ .Values.persistence.mountPath }}/conf/postgresql.conf"] + {{- end }} + {{- if and .Values.postgresql.pghba (not .Values.postgresql.config)}} + args: ["-c", "hba_file={{ .Values.persistence.mountPath }}/conf/pg_hba.conf"] + {{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + resources: +{{ toYaml .Values.resources | indent 10 }} + env: + - name: POSTGRESQL_PASSWORD + value: {{ .Values.postgresql.password | quote }} + - name: POSTGRESQL_USER + value: {{ .Values.postgresql.username | quote }} + - name: POSTGRESQL_DATABASE + value: {{ .Values.postgresql.database | quote }} + {{- if .Values.persistence.mountPath }} + - name: PGDATA + value: {{ .Values.postgresql.dataDir | quote }} + {{- end }} +{{- if .Values.extraEnv }} +{{ tpl (toYaml .Values.extraEnv) $ | indent 8 }} +{{- end }} + ports: + - name: postgresql + containerPort: {{ .Values.postgresql.port }} + livenessProbe: +{{ toYaml .Values.livenessProbe | indent 12 }} + readinessProbe: +{{ toYaml .Values.readinessProbe | indent 12 }} + volumeMounts: + {{- if .Values.persistence.enabled }} + - name: data + mountPath: {{ .Values.persistence.mountPath }} + subPath: {{ .Values.persistence.subPath }} + readOnly: false + {{- end }} + {{- if or (.Files.Glob "configs/pg_hba.conf") .Values.postgresql.pghba .Values.configMap }} + - name: postgresql-config-pghba + mountPath: {{ .Values.persistence.mountPath }}/conf/pg_hba.conf + subPath: pg_hba.conf + readOnly: false + {{- end }} + {{- if or (.Files.Glob "configs/postgresql.conf") .Values.postgresql.config .Values.configMap }} + - name: postgresql-config + mountPath: {{ .Values.persistence.mountPath }}/conf/postgresql.conf + subPath: postgresql.conf + readOnly: false + {{- end }} + volumes: + {{- if or (.Files.Glob "configs/pg_hba.conf") .Values.postgresql.pghba .Values.postgresql.configMap}} + - name: postgresql-config-pghba + configMap: + name: {{ template "postgresql.fullname" . }}-configuration + items: + - key: pg_hba.conf + path: pg_hba.conf + {{- end }} + {{- if or (.Files.Glob "configs/postgresql.conf") .Values.postgresql.config .Values.postgresql.configMap}} + - name: postgresql-config + configMap: + name: {{ template "postgresql.fullname" . }}-configuration + items: + - key: postgresql.conf + path: postgresql.conf + {{- end }} +{{- if and .Values.persistence.enabled .Values.persistence.existingClaim }} + - name: data + persistentVolumeClaim: +{{- with .Values.persistence.existingClaim }} + claimName: {{ tpl . $ }} +{{- end }} +{{- else if not .Values.persistence.enabled }} + - name: data + emptyDir: {} +{{- else if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} + volumeClaimTemplates: + - metadata: + name: data + {{- with .Values.persistence.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value }} + {{- end }} + {{- end }} + spec: + accessModes: + {{- range .Values.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- end }} diff --git a/helm/jira-cluster-db/values.yaml b/helm/jira-cluster-db/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fd87ec92e1914e07eb9e94762dd9e55a4dd996a0 --- /dev/null +++ b/helm/jira-cluster-db/values.yaml @@ -0,0 +1,49 @@ +--- +image: + repository: dsop/postgres-v9.6 + tag: latest + pullPolicy: IfNotPresent + +service: + type: ClusterIP + annotations: {} + +## Postgresql values +postgresql: + username: jira + password: jira + database: jira + port: 5432 + dataDir: /var/lib/postgresql/data + +volumePermissions: + enabled: true + image: + registry: docker.io + repository: bitnami/minideb + tag: latest + + pullPolicy: Always + + securityContext: + runAsUser: 0 + +securityContext: + enabled: true + fsGroup: 1001 + runAsUser: 1001 + +persistence: + enabled: true + mountPath: /var/lib/postgresql + subPath: "" + accessModes: [ReadWriteOnce] + ## Storage Capacity for persistent volume + size: 10Gi + annotations: {} + +resources: {} + +nodeSelector: {} + +tolerations: [] diff --git a/helm/jira-cluster-node/.helmignore b/helm/jira-cluster-node/.helmignore new file mode 100644 index 0000000000000000000000000000000000000000..50af0317254197a5a019f4ac2f8ecc223f93f5a7 --- /dev/null +++ b/helm/jira-cluster-node/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/jira-cluster-node/Chart.yaml b/helm/jira-cluster-node/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c3e5d5682bbf180f916ce0f65b679ede0000c588 --- /dev/null +++ b/helm/jira-cluster-node/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +version: 0.1.0 +appVersion: 8.5.9 +description: Project Management Tool for Agile Teams. +name: jira-cluster-node +keywords: +- atlassian +- jira +home: https://www.atlassian.com/software/jira +maintainers: + - name: Ascend Integrated diff --git a/helm/jira-cluster-node/templates/_helpers.tpl b/helm/jira-cluster-node/templates/_helpers.tpl new file mode 100644 index 0000000000000000000000000000000000000000..96904b49bcb2a370a4b80a60aa5780399ad65a41 --- /dev/null +++ b/helm/jira-cluster-node/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "jira.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "jira.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "jira.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/helm/jira-cluster-node/templates/deployment.yaml b/helm/jira-cluster-node/templates/deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..10d9d49627f5bda7a045831c1483650dabf68b28 --- /dev/null +++ b/helm/jira-cluster-node/templates/deployment.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "jira.fullname" . }} + labels: + app: {{ include "jira.name" . }} + chart: {{ template "jira.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ template "jira.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ include "jira.name" . }} + chart: {{ template "jira.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} + spec: + initContainers: + - name: chown-data-volume + image: busybox + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["chown", "10777:10777", "-R", "/jira-shared-home"] + volumeMounts: + - name: data + mountPath: /jira-shared-home + containers: + - name: {{ template "jira.fullname" . }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: JIRA_NODE_NAME + value: {{ .Values.jira_node_name | quote }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + - name: ehcachelsnr + containerPort: 40001 + protocol: TCP + - name: ehcachemtcst + containerPort: 4446 + protocol: TCP + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 420 + periodSeconds: 15 + timeoutSeconds: 3 + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 300 + periodSeconds: 15 + timeoutSeconds: 3 + volumeMounts: + - name: data + mountPath: /jira-shared-home + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + volumes: + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ .Values.persistence.existingClaim | default (include "jira.fullname" .) }} + {{- else }} + emptyDir: {} + {{- end -}} diff --git a/helm/jira-cluster-node/templates/ingress.yaml b/helm/jira-cluster-node/templates/ingress.yaml new file mode 100644 index 0000000000000000000000000000000000000000..abac201a430d8b37b70339da218b3438883884ee --- /dev/null +++ b/helm/jira-cluster-node/templates/ingress.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "jira.fullname" . -}} +{{- $servicePort := .Values.service.port -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "jira.name" . }} + chart: {{ template "jira.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: 8080 + {{- end }} +{{- end }} diff --git a/helm/jira-cluster-node/templates/pvc.yaml b/helm/jira-cluster-node/templates/pvc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..200d27a435e8ef14cd021cb43454e5282561d4ca --- /dev/null +++ b/helm/jira-cluster-node/templates/pvc.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ template "jira.fullname" . }} + labels: + app: {{ template "jira.name" . }} + chart: {{ template "jira.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- if .Values.persistence.storageClass }} +{{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" +{{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" +{{- end }} +{{- end }} +{{- end }} diff --git a/helm/jira-cluster-node/templates/service.yaml b/helm/jira-cluster-node/templates/service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0f184c74999160f8b47da747dd54e2291ffd3289 --- /dev/null +++ b/helm/jira-cluster-node/templates/service.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "jira.fullname" . }} + labels: + app: {{ template "jira.name" . }} + chart: {{ template "jira.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + - port: {{ .Values.service.ehcachelsnr }} + targetPort: ehcachelsnr + protocol: TCP + name: ehcachelsnr + - port: {{ .Values.service.ehcachemtcst }} + targetPort: ehcachemtcst + protocol: TCP + name: ehcachemtcst + selector: + app: {{ template "jira.name" . }} + release: {{ .Release.Name }} diff --git a/helm/jira-cluster-node/values.yaml b/helm/jira-cluster-node/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..44e36f3d867e20688f7d15346db89d8dfd472684 --- /dev/null +++ b/helm/jira-cluster-node/values.yaml @@ -0,0 +1,41 @@ +image: + repository: dsop/jira-node + tag: 8.5.9 + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 8080 + ehcachelsnr: 40001 + ehcachemtcst: 4446 + +ingress: + enabled: true + annotations: + { + kubernetes.io/ingress.class: nginx, + nginx.ingress.kubernetes.io/affinity: cookie + } + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + path: / + hosts: + - jira.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +persistence: + enabled: true + accessMode: ReadWriteMany + size: 8Gi + # existingClaim: existing-pvc + +resources: {} + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/scripts/docker-shared-components/LICENSE b/scripts/docker-shared-components/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..2237185e77c5d09cb92e849af1d1ac908185f51b --- /dev/null +++ b/scripts/docker-shared-components/LICENSE @@ -0,0 +1,13 @@ +Copyright © 2019 Atlassian Corporation Pty Ltd. + +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/scripts/docker-shared-components/README.md b/scripts/docker-shared-components/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8a1386016757c52e93115027838232d71589f642 --- /dev/null +++ b/scripts/docker-shared-components/README.md @@ -0,0 +1,36 @@ +# Overview + +This repository provides common utilities & components for building and testing Docker +images for Atlassian's Server and Data Center products. + +The following components are provided: + +### Image builds + +* Support tools + + Scripts for performing common diagnostic operations, i.e. taking thread dumps and heap + dumps. + +* Entrypoint helpers + + Common components for bootstrapping and starting apps. + +* README publishing + + Utility for publishing README's to Docker Hub, without relying on Docker Hub's own + automated builds. + +### Image testing + +* Fixtures + + Common testing fixtures that can be reused for testing Docker builds of Atlassian + apps. + +* Helpers + + Helper functions for parsing configuration files, checking running processes and + retrieving http responses. + + diff --git a/scripts/docker-shared-components/bitbucket-pipelines.yml b/scripts/docker-shared-components/bitbucket-pipelines.yml new file mode 100644 index 0000000000000000000000000000000000000000..8e2b4bf27c6bcc1e121281823d6edb45df003b67 --- /dev/null +++ b/scripts/docker-shared-components/bitbucket-pipelines.yml @@ -0,0 +1,54 @@ +image: atlassian/default-image:2 + +pipelines: + branches: + master: + - step: + name: Auto create PRs + script: + - > + export ACCESS_TOKEN=$(curl "https://bitbucket.org/site/oauth2/access_token" \ + --silent \ + --request POST \ + --user "${DOCKER_BOT_CLIENT_ID}:${DOCKER_BOT_CLIENT_SECRET}" \ + --data 'grant_type=client_credentials' \ + --data 'scopes=repository' | jq --raw-output '.access_token') + - > + function update_shared_components() { + local REPO="$1" + local REVIEWERS=$(curl "https://api.bitbucket.org/2.0/repositories/${REPO}/default-reviewers" \ + --silent \ + --header "Authorization: Bearer ${ACCESS_TOKEN}" \ + --header "Content-Type: application/json" | jq --raw-output '.values') + git clone "https://x-token-auth:${ACCESS_TOKEN}@bitbucket.org/${REPO}" ~/${REPO} + cd ~/${REPO} + git checkout -B update-shared-components + git submodule update --init --recursive + git submodule update --recursive --remote + git add . + git -c "user.name=Atlassian Docker Bot" -c "user.email=$DOCKER_BOT_EMAIL" commit -m "Update shared components" + if [[ $(git ls-remote --heads origin update-shared-components) ]]; then + local GIT_OPTS="--force-with-lease" + fi + git push ${GIT_OPTS} origin update-shared-components + local PR_DATA='{ + "title": "Update shared components", + "close_source_branch": true, + "source": { + "branch": { + "name": "update-shared-components" + } + }, + "reviewers": '${REVIEWERS}' + }' + curl "https://api.bitbucket.org/2.0/repositories/${REPO}/pullrequests" \ + --globoff \ + --request POST \ + --header "Authorization: Bearer ${ACCESS_TOKEN}" \ + --header "Content-Type: application/json" \ + --data "${PR_DATA}" + } + - update_shared_components atlassian-docker/docker-atlassian-bitbucket-server + - update_shared_components atlassian-docker/docker-atlassian-confluence-server + - update_shared_components atlassian-docker/docker-atlassian-jira + - update_shared_components atlassian-docker/docker-atlassian-crowd diff --git a/scripts/docker-shared-components/image/push-readme.py b/scripts/docker-shared-components/image/push-readme.py new file mode 100644 index 0000000000000000000000000000000000000000..0c3d4006e7a5cac94e63dae4b3a8dca1ee998c3c --- /dev/null +++ b/scripts/docker-shared-components/image/push-readme.py @@ -0,0 +1,33 @@ +import logging +import os + +import requests + + +logging.basicConfig(level=logging.INFO) + + +DOCKER_REPO = os.environ.get('DOCKER_REPO') +DOCKER_USERNAME = os.environ.get('DOCKER_USERNAME') +DOCKER_PASSWORD = os.environ.get('DOCKER_PASSWORD') +README_FILE = os.environ.get('README_FILE') or 'README.md' + + +logging.info('Generating Docker Hub JWT') +data = {'username': DOCKER_USERNAME, 'password': DOCKER_PASSWORD} +r = requests.post('https://hub.docker.com/v2/users/login/', json=data) +docker_token = r.json().get('token') + +logging.info(f'Updating Docker Hub description for {DOCKER_REPO}') +with open(README_FILE) as f: + full_description = f.read() +data = {'registry': 'registry-1.docker.io', 'full_description': full_description} +headers = {'Authorization': f'JWT {docker_token}'} +r = requests.patch(f'https://hub.docker.com/v2/repositories/{DOCKER_REPO}/', + json=data, headers=headers) + +if r.status_code == requests.codes.ok: + logging.info(f'Successfully updated {README_FILE} for {DOCKER_REPO}') +else: + logging.info(f'Unable to update {README_FILE} for {DOCKER_REPO}, response code: {r.status_code}') + r.raise_for_status() \ No newline at end of file diff --git a/scripts/docker-shared-components/support/common.sh b/scripts/docker-shared-components/support/common.sh new file mode 100644 index 0000000000000000000000000000000000000000..f1abe2c5d1b2107dc492ed49f4a7554e35bd4041 --- /dev/null +++ b/scripts/docker-shared-components/support/common.sh @@ -0,0 +1,57 @@ +# ------------------------------------------------------------------------------------- +# Common bootstrapping for support scripts (get app details: home directory, PID, etc.) +# ------------------------------------------------------------------------------------- + + +# Set up Java utils +JCMD="${JAVA_HOME}/bin/jcmd" + +# Set up app info +APP_NAME="$(set | grep '_INSTALL_DIR' | awk -F'_' '{print $1}')" + +# Get value of _INSTALL_DIR +function get_app_install_dir { + local APP_INSTALL_DIR=${APP_NAME}_INSTALL_DIR + echo ${!APP_INSTALL_DIR} +} + +# Get value of _HOME +function get_app_home { + local APP_HOME=${APP_NAME}_HOME + echo ${!APP_HOME} +} + + +# Get app PID +case "${APP_NAME}" in + BITBUCKET ) + BOOTSTRAP_PROC="com.atlassian.bitbucket.internal.launcher.BitbucketServerLauncher" + ;; + * ) + BOOTSTRAP_PROC="org.apache.catalina.startup.Bootstrap" + ;; +esac + +APP_PID=$(${JCMD} | grep "${BOOTSTRAP_PROC}" | awk '{print $1}') + + + +# Set valid getopt options +function set_valid_options { + OPTS=$(getopt -o "$1" --long "$2" -n 'parse-options' -- "$@") + if [ $? != 0 ]; then + echo "Failed parsing options." >&2 + exit 1 + fi + eval set -- "$OPTS" +} + + +# Run command(s) +function run_as_runuser { + if [ $(id -u) = 0 ]; then + su "${RUN_USER}" -c '"$@"' -- argv0 "$@" + else + $@ + fi +} diff --git a/scripts/docker-shared-components/support/heap-dump.sh b/scripts/docker-shared-components/support/heap-dump.sh new file mode 100755 index 0000000000000000000000000000000000000000..7341f306596386a4c6dc97d643a16396896f5c66 --- /dev/null +++ b/scripts/docker-shared-components/support/heap-dump.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# ------------------------------------------------------------------------------------- +# Heap collector for containerized Atlassian applications +# +# This script can be run via `docker exec` to easily trigger the collection of a heap +# dump from the containerized application. For example: +# +# $ docker exec -it my_jira /opt/atlassian/support/heap-dump.sh +# +# A heap dump will be written to $APP_HOME/heap.bin. If a file already exists at this +# location, use -f/--force to overwrite the existing heap dump file. +# +# ------------------------------------------------------------------------------------- + + +set -euo pipefail + + +# Set up common vars like APP_NAME, APP_HOME, APP_PID +SCRIPT_DIR=$(dirname "$0") +source "${SCRIPT_DIR}/common.sh" + +# Set up script opts +set_valid_options "f" "force" + +# Set defaults +OVERWRITE="false" + +# Parse opts +while true; do + case "${1-}" in + -f | --force ) OVERWRITE="true"; shift ;; + * ) break ;; + esac +done + + + +echo "Atlassian heap dump collector" +echo "App: ${APP_NAME}" +echo "Run user: ${RUN_USER}" +echo + +OUT_FILE="$(get_app_home)/heap.bin" + +if [[ -f "${OUT_FILE}" ]]; then + echo "A previous heap dump already exists at ${OUT_FILE}." + if [[ "${OVERWRITE}" == "true" ]]; then + echo "Removing previous heap dump file" + echo + rm "${OUT_FILE}" + else + echo "Use -f/--force to overwrite the existing heap dump." + exit + fi +fi + +echo "Generating heap dump" +run_as_runuser ${JCMD} ${APP_PID} GC.heap_dump -all ${OUT_FILE} > /dev/null +echo +echo "Heap dump has been written to ${OUT_FILE}" diff --git a/scripts/docker-shared-components/support/thread-dumps.sh b/scripts/docker-shared-components/support/thread-dumps.sh new file mode 100755 index 0000000000000000000000000000000000000000..9d76d1a1a68ab79bafa96f80f6dd7166a5ae0172 --- /dev/null +++ b/scripts/docker-shared-components/support/thread-dumps.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# ------------------------------------------------------------------------------------- +# Thread dumps collector for containerized Atlassian applications +# +# This script can be run via `docker exec` to easily trigger the collection of thread +# dumps from the containerized application. For example: +# +# $ docker exec my_jira /opt/atlassian/support/thread-dumps.sh +# +# By default this script will collect 10 thread dumps at a 5 second interval. This can +# be overridden by passing a custom value for the count and interval, respectively. For +# example, to collect 20 thread dumps at a 3 second interval: +# +# $ docker exec my_jira /opt/atlassian/support/thread-dumps.sh -c 20 -i 3 +# +# Note: By default this script will capture output from top run in 'Thread-mode'. This can +# be disabled by passing --no-top +# ------------------------------------------------------------------------------------- + + +set -euo pipefail + + +# Set up common vars like APP_NAME, APP_HOME, APP_PID +SCRIPT_DIR=$(dirname "$0") +source "${SCRIPT_DIR}/common.sh" + +# Set up script opts +set_valid_options "c:i:n" "count:,interval:,no-top" + +# Set defaults +COUNT="10" +INTERVAL="5" +NO_TOP="false" + +# Parse opts +while true; do + case "${1-}" in + -c | --count ) COUNT="$2"; shift 2 ;; + -i | --interval ) INTERVAL="$2"; shift 2 ;; + -n | --no-top ) NO_TOP="true"; shift ;; + * ) break ;; + esac +done + + + +echo "Atlassian thread dump collector" +echo "App: ${APP_NAME}" +echo "Run user: ${RUN_USER}" +echo +echo "${COUNT} thread dumps will be generated at a ${INTERVAL} second interval" +if [[ "${NO_TOP}" == "false" ]]; then + echo "top 'Threads-mode' output will also be collected for ${APP_NAME} with every thread dump" +fi +echo + +OUT_DIR="$(get_app_home)/thread_dumps/$(date +'%Y-%m-%d_%H-%M-%S')" +run_as_runuser mkdir -p ${OUT_DIR} + +for i in $(seq ${COUNT}); do + echo "Generating thread dump ${i} of ${COUNT}" + if [[ "${NO_TOP}" == "false" ]]; then + run_as_runuser top -b -H -p $APP_PID -n 1 > "${OUT_DIR}/${APP_NAME}_CPU_USAGE.$(date +%s).txt" + fi + run_as_runuser ${JCMD} ${APP_PID} Thread.print -l > "${OUT_DIR}/${APP_NAME}_THREADS.$(date +%s).txt" + if [[ ! "${i}" == "${COUNT}" ]]; then + sleep ${INTERVAL} + fi +done + +echo +echo "Thread dumps have been written to ${OUT_DIR}" diff --git a/scripts/docker-shared-components/support/waitport b/scripts/docker-shared-components/support/waitport new file mode 100755 index 0000000000000000000000000000000000000000..84b4037cdc5776e7aef0a4cdafa15ae44d04dbb0 --- /dev/null +++ b/scripts/docker-shared-components/support/waitport @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +host=$1 +port=$2 +secs=120 + +echo -n "Waiting for TCP connection to $host:$port..." + +for i in `seq $secs`; do + if nc -z $host $port > /dev/null ; then + echo OK + exit 0 + fi + + echo -n . + /bin/sleep 1 + +done + +echo FAILED +exit -1 diff --git a/scripts/docker-shared-components/tests/Dockerfile-test b/scripts/docker-shared-components/tests/Dockerfile-test new file mode 100644 index 0000000000000000000000000000000000000000..f41d30975ff152e5429d10b6ef973923de0d22a0 --- /dev/null +++ b/scripts/docker-shared-components/tests/Dockerfile-test @@ -0,0 +1,4 @@ +ARG BASE_IMAGE=atlassian/jira-software:latest +FROM $BASE_IMAGE + +COPY support /opt/atlassian/support diff --git a/scripts/docker-shared-components/tests/conftest.py b/scripts/docker-shared-components/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..0b18cc0a6c174d9d9dcb5073241542b375a39f71 --- /dev/null +++ b/scripts/docker-shared-components/tests/conftest.py @@ -0,0 +1,3 @@ +import pytest + +from fixtures import docker_cli, image, run_user \ No newline at end of file diff --git a/scripts/docker-shared-components/tests/fixtures.py b/scripts/docker-shared-components/tests/fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..c48d5cfc4702c4791462a5022904080b2c22e8f0 --- /dev/null +++ b/scripts/docker-shared-components/tests/fixtures.py @@ -0,0 +1,64 @@ +import pytest + +import os + +import docker +import requests + + +DOCKERFILE = os.environ.get('DOCKERFILE') or 'Dockerfile' +DOCKERFILE_BUILDARGS = os.environ.get('DOCKERFILE_BUILDARGS') +DOCKERFILE_VERSION_ARG = os.environ.get('DOCKERFILE_VERSION_ARG') +MAC_PRODUCT_KEY = os.environ.get('MAC_PRODUCT_KEY') or 'docker-testapp' + + +def parse_buildargs(buildargs): + if buildargs is None or len(buildargs) == 0: + return {} + return dict(item.split("=") for item in buildargs.split(",")) + + +def make_image(): + buildargs = parse_buildargs(DOCKERFILE_BUILDARGS) + if MAC_PRODUCT_KEY != 'docker-testapp': + r = requests.get(f'https://marketplace.atlassian.com/rest/2/products/key/{MAC_PRODUCT_KEY}/versions/latest') + version = r.json().get('name') + buildargs[DOCKERFILE_VERSION_ARG] = version + docker_cli = docker.from_env() + tag = ''.join(ch for ch in DOCKERFILE if ch.isalnum()) + image = docker_cli.images.build(path='.', + tag=f'{MAC_PRODUCT_KEY}:{tag}'.lower(), + buildargs=buildargs, + dockerfile=DOCKERFILE, + rm=True)[0] + return image + + +def get_run_user(): + i = make_image() + image_env = {k:v for k,v in (x.split('=') for x in i.attrs['ContainerConfig']['Env'])} + run_user = f'{image_env["RUN_UID"]}:{image_env["RUN_GID"]}' + return run_user + + +# This fixture returns a temporary Docker CLI that cleans up running test containers after each test +@pytest.fixture +def docker_cli(): + docker_cli = docker.from_env() + yield docker_cli + for container in docker_cli.containers.list(): + for tag in container.image.tags: + if tag.startswith(MAC_PRODUCT_KEY): + container.remove(force=True) + + +# This fixture returns an image for the Docker build being tested +@pytest.fixture(scope='module') +def image(): + return make_image() + + +# This fixture returns the uid:gid for the Docker build being tested +@pytest.fixture(scope='module', params=['0:0', get_run_user()]) +def run_user(request): + return request.param \ No newline at end of file diff --git a/scripts/docker-shared-components/tests/helpers.py b/scripts/docker-shared-components/tests/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..01b9a9e9ea4bbf93eb06eed21b4c5a699bcc9b52 --- /dev/null +++ b/scripts/docker-shared-components/tests/helpers.py @@ -0,0 +1,78 @@ +import time +import xml.etree.ElementTree as etree + +import requests +import testinfra + + +# Helper functions to get config values from support scripts +def get_app_home(container): + cmd = "/bin/bash -c 'source /opt/atlassian/support/common.sh && get_app_home'" + home = container.check_output(cmd) + return home + +def get_app_install_dir(container): + cmd = "/bin/bash -c 'source /opt/atlassian/support/common.sh && get_app_install_dir'" + home = container.check_output(cmd) + return home + +def get_bootstrap_proc(container): + cmd = "/bin/bash -c 'source /opt/atlassian/support/common.sh && echo ${BOOTSTRAP_PROC}'" + proc = container.check_output(cmd) + return proc + +# Run an image and wrap it in a TestInfra host for convenience. +# FIXME: There's probably a way to turn this into a fixture with parameters. +def run_image(docker_cli, image, **kwargs): + container = docker_cli.containers.run(image, detach=True, **kwargs) + return testinfra.get_host("docker://"+container.id) + +# TestInfra's process command doesn't seem to work for arg matching +def get_procs(container): + ps = container.run('ps -axo args') + return ps.stdout.split('\n') + +def parse_properties(container, properties): + properties_raw = container.file(properties).content + properties_str = properties_raw.decode().strip().split('\n') + return dict(item.split("=") for item in properties_str) + +def parse_xml(container, xml): + return etree.fromstring(container.file(xml).content) + +def wait_for_proc(container, proc_str, max_wait=10): + waited = 0 + while waited < max_wait: + procs = list(filter(lambda p: proc_str in p, get_procs(container))) + if len(procs) > 0: + return procs[0] + time.sleep(0.1) + waited += 0.1 + + raise TimeoutError("Failed to find target process") + +def wait_for_file(container, path, max_wait=10): + waited = 0 + while waited < max_wait: + if container.file(path).exists: + return + time.sleep(0.1) + waited += 0.1 + + raise TimeoutError("Failed to find target process") + +def wait_for_http_response(url, expected_status=200, expected_state=None, max_wait=20): + timeout = time.time() + max_wait + while time.time() < timeout: + try: + r = requests.get(url) + except requests.exceptions.ConnectionError: + pass + else: + if r.status_code == expected_status: + if expected_state is not None: + state = r.json().get('state') + assert state in expected_state + return + time.sleep(1) + raise TimeoutError \ No newline at end of file diff --git a/scripts/docker-shared-components/tests/requirements.txt b/scripts/docker-shared-components/tests/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..9eb3bafddb699a08960f537dfc86a45a509466ce --- /dev/null +++ b/scripts/docker-shared-components/tests/requirements.txt @@ -0,0 +1,20 @@ +atomicwrites==1.3.0 +attrs==19.1.0 +certifi==2019.6.16 +chardet==3.0.4 +docker==4.0.2 +idna==2.8 +importlib-metadata==0.19 +more-itertools==7.2.0 +packaging==19.1 +pluggy==0.12.0 +py==1.8.0 +pyparsing==2.4.2 +pytest==5.0.1 +requests==2.22.0 +six==1.12.0 +testinfra==3.0.6 +urllib3==1.25.3 +wcwidth==0.1.7 +websocket-client==0.56.0 +zipp==0.5.2 diff --git a/scripts/docker-shared-components/tests/test_support.py b/scripts/docker-shared-components/tests/test_support.py new file mode 100644 index 0000000000000000000000000000000000000000..1e95192c3d36e2f4ef99c6a245f7d9f04f0dc829 --- /dev/null +++ b/scripts/docker-shared-components/tests/test_support.py @@ -0,0 +1,75 @@ +import pytest + +from helpers import get_app_home, get_bootstrap_proc, run_image, wait_for_proc + + +def test_thread_dumps(docker_cli, image, run_user): + COUNT = 3 + INTERVAL = 1 + container = run_image(docker_cli, image, user=run_user) + wait_for_proc(container, get_bootstrap_proc(container)) + + thread_cmd = f'/opt/atlassian/support/thread-dumps.sh --count {COUNT} --interval {INTERVAL}' + container.run(thread_cmd) + + find_thread_cmd = f'find {get_app_home(container)} -name "*_THREADS.*.txt"' + thread_dumps = container.run(find_thread_cmd).stdout.splitlines() + assert len(thread_dumps) == COUNT + + find_top_cmd = f'find {get_app_home(container)} -name "*_CPU_USAGE.*.txt"' + top_dumps = container.run(find_top_cmd).stdout.splitlines() + assert len(top_dumps) == COUNT + +def test_thread_dumps_no_top(docker_cli, image, run_user): + COUNT = 3 + INTERVAL = 1 + container = run_image(docker_cli, image, user=run_user) + wait_for_proc(container, get_bootstrap_proc(container)) + + thread_cmd = f'/opt/atlassian/support/thread-dumps.sh --no-top --count {COUNT} --interval {INTERVAL}' + container.run(thread_cmd) + + find_thread_cmd = f'find {get_app_home(container)} -name "*_THREADS.*.txt"' + thread_dumps = container.run(find_thread_cmd).stdout.splitlines() + assert len(thread_dumps) == COUNT + + find_top_cmd = f'find {get_app_home(container)} -name "*_CPU_USAGE.*.txt"' + top_dumps = container.run(find_top_cmd).stdout.splitlines() + assert len(top_dumps) == 0 + +def test_heap_dump(docker_cli, image, run_user): + container = run_image(docker_cli, image, user=run_user) + wait_for_proc(container, get_bootstrap_proc(container)) + + heap_cmd = f'/opt/atlassian/support/heap-dump.sh' + container.run(heap_cmd) + + ls_cmd = f'ls -la {get_app_home(container)}/heap.bin' + heap_dump = container.run(ls_cmd).stdout.splitlines() + assert len(heap_dump) == 1 + +def test_heap_dump_overwrite_false(docker_cli, image, run_user): + container = run_image(docker_cli, image, user=run_user) + wait_for_proc(container, get_bootstrap_proc(container)) + + heap_cmd = f'/opt/atlassian/support/heap-dump.sh' + ls_cmd = f'ls -la --time-style=full-iso {get_app_home(container)}/heap.bin' + + container.run(heap_cmd) + heap_dump_1 = container.run(ls_cmd).stdout.splitlines() + container.run(heap_cmd) + heap_dump_2 = container.run(ls_cmd).stdout.splitlines() + assert heap_dump_1 == heap_dump_2 + +def test_heap_dump_overwrite_true(docker_cli, image, run_user): + container = run_image(docker_cli, image, user=run_user) + wait_for_proc(container, get_bootstrap_proc(container)) + + heap_cmd = f'/opt/atlassian/support/heap-dump.sh --force' + ls_cmd = f'ls -la {get_app_home(container)}/heap.bin' + + container.run(heap_cmd) + heap_dump_1 = container.run(ls_cmd).stdout.splitlines() + container.run(heap_cmd) + heap_dump_2 = container.run(ls_cmd).stdout.splitlines() + assert heap_dump_1 != heap_dump_2 diff --git a/scripts/entrypoint.py b/scripts/entrypoint.py new file mode 100644 index 0000000000000000000000000000000000000000..48a7ebee6747c8bcbb4df7dff8ff6d62926afc16 --- /dev/null +++ b/scripts/entrypoint.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 -B + +import os + +from entrypoint_helpers import env, gen_cfg, gen_container_id, str2bool, start_app + + +RUN_USER = env['run_user'] +RUN_GROUP = env['run_group'] +JIRA_INSTALL_DIR = env['jira_install_dir'] +JIRA_HOME = env['jira_home'] + +gen_container_id() +if os.stat('/etc/container_id').st_size == 0: + gen_cfg('container_id.j2', '/etc/container_id', + user=RUN_USER, group=RUN_GROUP, overwrite=True) +gen_cfg('server.xml.j2', f'{JIRA_INSTALL_DIR}/conf/server.xml') +gen_cfg('seraph-config.xml.j2', + f'{JIRA_INSTALL_DIR}/atlassian-jira/WEB-INF/classes/seraph-config.xml') +gen_cfg('dbconfig.xml.j2', f'{JIRA_HOME}/dbconfig.xml', + user=RUN_USER, group=RUN_GROUP, overwrite=False) +if str2bool(env.get('clustered')): + gen_cfg('cluster.properties.j2', f'{JIRA_HOME}/cluster.properties', + user=RUN_USER, group=RUN_GROUP, overwrite=False) + +start_app(f'{JIRA_INSTALL_DIR}/bin/start-jira.sh -fg', JIRA_HOME, name='Jira') diff --git a/scripts/entrypoint_helpers.py b/scripts/entrypoint_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..9c874a3e308538419dbc41dd45679b3595f60b16 --- /dev/null +++ b/scripts/entrypoint_helpers.py @@ -0,0 +1,97 @@ +import sys +import os +import shutil +import logging +import jinja2 as j2 +import uuid +import base64 + + +logging.basicConfig(level=logging.DEBUG) + + +###################################################################### +# Setup inputs and outputs + +# Import all ATL_* and Dockerfile environment variables. We lower-case +# these for compatability with Ansible template convention. We also +# support CATALINA variables from older versions of the Docker images +# for backwards compatability, if the new version is not set. +env = {k.lower(): v + for k, v in os.environ.items()} + + +# Setup Jinja2 for templating +jenv = j2.Environment( + loader=j2.FileSystemLoader('/opt/atlassian/etc/'), + autoescape=j2.select_autoescape(['xml'])) + + +###################################################################### +# Utils + +def set_perms(path, user, group, mode): + shutil.chown(path, user=user, group=group) + os.chmod(path, mode) + for dirpath, dirnames, filenames in os.walk(path): + shutil.chown(dirpath, user=user, group=group) + os.chmod(dirpath, mode) + for filename in filenames: + shutil.chown(os.path.join(dirpath, filename), user=user, group=group) + os.chmod(os.path.join(dirpath, filename), mode) + +def check_perms(path, uid, gid, mode): + stat = os.stat(path) + return all([ + stat.st_uid == int(uid), + stat.st_gid == int(gid), + stat.st_mode & mode == mode + ]) + +def gen_cfg(tmpl, target, user='root', group='root', mode=0o644, overwrite=True): + if not overwrite and os.path.exists(target): + logging.info(f"{target} exists; skipping.") + return + + logging.info(f"Generating {target} from template {tmpl}") + cfg = jenv.get_template(tmpl).render(env) + try: + with open(target, 'w') as fd: + fd.write(cfg) + except (OSError, PermissionError): + logging.warning(f"Container not started as root. Bootstrapping skipped for '{target}'") + else: + set_perms(target, user, group, mode) + +def gen_container_id(): + env['uuid'] = uuid.uuid4().hex + with open('/etc/container_id') as fd: + lcid = fd.read() + if lcid != '': + env['local_container_id'] = lcid + +def str2bool(v): + if str(v).lower() in ('yes', 'true', 't', 'y', '1'): + return True + return False + + +###################################################################### +# Start App as the correct user + +def start_app(start_cmd, home_dir, name='app'): + if os.getuid() == 0: + if str2bool(env.get('set_permissions') or True) and check_perms(home_dir, env['run_uid'], env['run_gid'], 0o700) is False: + set_perms(home_dir, env['run_user'], env['run_group'], 0o700) + logging.info(f"User is currently root. Will change directory ownership and downgrade run user to {env['run_user']}") + else: + logging.info(f"User is currently root. Will downgrade run user to {env['run_user']}") + + cmd = '/bin/su' + args = [cmd, env['run_user'], '-c', start_cmd] + else: + cmd = '/bin/sh' + args = [cmd, '-c', start_cmd] + + logging.info(f"Running {name} with command '{cmd}', arguments {args}") + os.execv(cmd, args) \ No newline at end of file diff --git a/scripts/shared-components b/scripts/shared-components new file mode 120000 index 0000000000000000000000000000000000000000..b18b221cd40889f310a10c409277f2ed025c6384 --- /dev/null +++ b/scripts/shared-components @@ -0,0 +1 @@ +docker-shared-components \ No newline at end of file