diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5bfc329 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "shared-components"] + path = shared-components + url = https://bitbucket.org/atlassian-docker/docker-shared-components.git diff --git a/Dockerfile b/Dockerfile index c35fb03..8d99ccd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,14 @@ ARG BASE_IMAGE=adoptopenjdk:11-hotspot FROM $BASE_IMAGE -ENV RUN_USER confluence -ENV RUN_GROUP confluence -ENV RUN_UID 2002 -ENV RUN_GID 2002 +ENV RUN_USER confluence +ENV RUN_GROUP confluence +ENV RUN_UID 2002 +ENV RUN_GID 2002 # https://confluence.atlassian.com/doc/confluence-home-and-other-important-directories-590259707.html -ENV CONFLUENCE_HOME /var/atlassian/application-data/confluence -ENV CONFLUENCE_INSTALL_DIR /opt/atlassian/confluence +ENV CONFLUENCE_HOME /var/atlassian/application-data/confluence +ENV CONFLUENCE_INSTALL_DIR /opt/atlassian/confluence WORKDIR $CONFLUENCE_HOME @@ -16,12 +16,12 @@ WORKDIR $CONFLUENCE_HOME EXPOSE 8090 EXPOSE 8091 -CMD ["/entrypoint.py", "-fg"] +CMD ["/entrypoint.py"] ENTRYPOINT ["/sbin/tini", "--"] RUN apt-get update \ - && apt-get install -y --no-install-recommends fontconfig python3 python3-jinja2 \ - && apt-get clean autoclean && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* + && apt-get install -y --no-install-recommends fontconfig python3 python3-jinja2 \ + && apt-get clean autoclean && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* ARG TINI_VERSION=v0.18.0 ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /sbin/tini @@ -34,18 +34,20 @@ RUN groupadd --gid ${RUN_GID} ${RUN_GROUP} \ && useradd --uid ${RUN_UID} --gid ${RUN_GID} --home-dir ${CONFLUENCE_HOME} --shell /bin/bash ${RUN_USER} \ && echo PATH=$PATH > /etc/environment \ \ - && mkdir -p ${CONFLUENCE_INSTALL_DIR} \ - && curl -L --silent ${DOWNLOAD_URL} | tar -xz --strip-components=1 -C "${CONFLUENCE_INSTALL_DIR}" \ - && chmod -R "u=rwX,g=rX,o=rX" ${CONFLUENCE_INSTALL_DIR}/ \ - && chown -R root. ${CONFLUENCE_INSTALL_DIR}/ \ - && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/logs \ - && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/temp \ - && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/work \ - && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_HOME} \ + && mkdir -p ${CONFLUENCE_INSTALL_DIR} \ + && curl -L --silent ${DOWNLOAD_URL} | tar -xz --strip-components=1 -C "${CONFLUENCE_INSTALL_DIR}" \ + && chmod -R "u=rwX,g=rX,o=rX" ${CONFLUENCE_INSTALL_DIR}/ \ + && chown -R root. ${CONFLUENCE_INSTALL_DIR}/ \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/logs \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/temp \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/work \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_HOME} \ \ && sed -i -e 's/-Xms\([0-9]\+[kmg]\) -Xmx\([0-9]\+[kmg]\)/-Xms\${JVM_MINIMUM_MEMORY:=\1} -Xmx\${JVM_MAXIMUM_MEMORY:=\2} \${JVM_SUPPORT_RECOMMENDED_ARGS} -Dconfluence.home=\${CONFLUENCE_HOME}/g' ${CONFLUENCE_INSTALL_DIR}/bin/setenv.sh VOLUME ["${CONFLUENCE_HOME}"] # Must be declared after setting perms -COPY entrypoint.py /entrypoint.py -COPY config/* /opt/atlassian/etc/ +COPY entrypoint.py \ + shared-components/image/entrypoint_helpers.py / +COPY shared-components/support /opt/atlassian/support +COPY config/* /opt/atlassian/etc/ diff --git a/Dockerfile-alpine b/Dockerfile-alpine index 5cf626c..c5b5f7e 100644 --- a/Dockerfile-alpine +++ b/Dockerfile-alpine @@ -1,13 +1,13 @@ FROM adoptopenjdk/openjdk8:alpine -ENV RUN_USER confluence -ENV RUN_GROUP confluence -ENV RUN_UID 2002 -ENV RUN_GID 2002 +ENV RUN_USER confluence +ENV RUN_GROUP confluence +ENV RUN_UID 2002 +ENV RUN_GID 2002 # https://confluence.atlassian.com/doc/confluence-home-and-other-important-directories-590259707.html -ENV CONFLUENCE_HOME /var/atlassian/application-data/confluence -ENV CONFLUENCE_INSTALL_DIR /opt/atlassian/confluence +ENV CONFLUENCE_HOME /var/atlassian/application-data/confluence +ENV CONFLUENCE_INSTALL_DIR /opt/atlassian/confluence WORKDIR $CONFLUENCE_HOME @@ -15,7 +15,7 @@ WORKDIR $CONFLUENCE_HOME EXPOSE 8090 EXPOSE 8091 -CMD ["/entrypoint.py", "-fg"] +CMD ["/entrypoint.py"] ENTRYPOINT ["/sbin/tini", "--"] RUN apk add --no-cache ca-certificates wget curl openssh bash procps openssl perl ttf-dejavu tini python3 py3-jinja2 @@ -32,18 +32,20 @@ ARG DOWNLOAD_URL=https://product-downloads.atlassian.com/software/confluence/dow RUN addgroup -g ${RUN_GID} ${RUN_GROUP} \ && adduser -u ${RUN_UID} -G ${RUN_GROUP} -h ${CONFLUENCE_HOME} -s /bin/bash -D ${RUN_USER} \ \ - && mkdir -p ${CONFLUENCE_INSTALL_DIR} \ - && curl -L --silent ${DOWNLOAD_URL} | tar -xz --strip-components=1 -C "${CONFLUENCE_INSTALL_DIR}" \ - && chmod -R "u=rwX,g=rX,o=rX" ${CONFLUENCE_INSTALL_DIR}/ \ - && chown -R root. ${CONFLUENCE_INSTALL_DIR}/ \ - && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/logs \ - && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/temp \ - && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/work \ - && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_HOME} \ + && mkdir -p ${CONFLUENCE_INSTALL_DIR} \ + && curl -L --silent ${DOWNLOAD_URL} | tar -xz --strip-components=1 -C "${CONFLUENCE_INSTALL_DIR}" \ + && chmod -R "u=rwX,g=rX,o=rX" ${CONFLUENCE_INSTALL_DIR}/ \ + && chown -R root. ${CONFLUENCE_INSTALL_DIR}/ \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/logs \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/temp \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_INSTALL_DIR}/work \ + && chown -R ${RUN_USER}:${RUN_GROUP} ${CONFLUENCE_HOME} \ \ && sed -i -e 's/-Xms\([0-9]\+[kmg]\) -Xmx\([0-9]\+[kmg]\)/-Xms\${JVM_MINIMUM_MEMORY:=\1} -Xmx\${JVM_MAXIMUM_MEMORY:=\2} \${JVM_SUPPORT_RECOMMENDED_ARGS} -Dconfluence.home=\${CONFLUENCE_HOME}/g' ${CONFLUENCE_INSTALL_DIR}/bin/setenv.sh VOLUME ["${CONFLUENCE_HOME}"] # Must be declared after setting perms -COPY entrypoint.py /entrypoint.py -COPY config/* /opt/atlassian/etc/ +COPY entrypoint.py \ + shared-components/image/entrypoint_helpers.py / +COPY shared-components/support /opt/atlassian/support +COPY config/* /opt/atlassian/etc/ diff --git a/README.md b/README.md index 9e1a311..bbdea5f 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,13 @@ management technology, and is beyond the scope of this documentation. The multicast address the cluster will communicate on. +## Container Configuration + +* `SET_PERMISSIONS` (default: true) + + Define whether to set home directory permissions on startup. Set to `false` to disable + this behaviour. + ## Advanced Configuration As mentioned at the top of this section, the settings from the environment are @@ -360,6 +367,45 @@ supported for use in production). For example, `atlassian/confluence-server:6.13-ubuntu-18.04-adoptopenjdk8` will install the latest 6.13.x version with AdoptOpenJDK 8. +# Troubleshooting + +These images include built-in scripts to assist in performing common JVM diagnostic tasks. + +## Thread dumps + +`/opt/atlassian/support/thread-dumps.sh` can be run via `docker exec` to easily trigger the collection of thread +dumps from the containerized application. For example: + + docker exec my_container /opt/atlassian/support/thread-dumps.sh + +By default this script will collect 10 thread dumps at 5 second intervals. This can +be overridden by passing a custom value for the count and interval, by using `-c` / `--count` +and `-i` / `--interval` respectively. For example, to collect 20 thread dumps at 3 second intervals: + + docker exec my_container /opt/atlassian/support/thread-dumps.sh --count 20 --interval 3 + +Thread dumps will be written to `$APP_HOME/thread_dumps/`. + +Note: By default this script will also capture output from top run in 'Thread-mode'. This can +be disabled by passing `-n` / `--no-top` + +## Heap dump + +`/opt/atlassian/support/heap-dump.sh` can be run via `docker exec` to easily trigger the collection of a heap +dump from the containerized application. For example: + + docker exec my_container /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. + +## Manual diagnostics + +The `jcmd` utility is also included in these images and can be used by starting a `bash` shell +in the running container: + + docker exec -it my_container /bin/bash + # Support These Confluence Docker images are presented as a technical preview, and not diff --git a/bin/push-readme.py b/bin/push-readme.py deleted file mode 100644 index e4fd067..0000000 --- a/bin/push-readme.py +++ /dev/null @@ -1,32 +0,0 @@ -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://cloud.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}') \ No newline at end of file diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index d3576e1..e9c9ec5 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -8,6 +8,7 @@ pipelines: services: - docker script: + - git submodule update --init --recursive - export START_VERSION='6' - export END_VERSION='7' - export DEFAULT_RELEASE='false' @@ -24,6 +25,7 @@ pipelines: services: - docker script: + - git submodule update --init --recursive - export START_VERSION='6' - export END_VERSION='7.1' - export DEFAULT_RELEASE='true' @@ -40,6 +42,7 @@ pipelines: services: - docker script: + - git submodule update --init --recursive - export START_VERSION='7.1' - export END_VERSION='8' - export DEFAULT_RELEASE='true' @@ -58,6 +61,7 @@ pipelines: services: - docker script: + - git submodule update --init --recursive - echo ${DOCKER_PASSWORD} | docker login --username ${DOCKER_USERNAME} --password-stdin - > docker build -t atlassian/confluence-server:${DOCKER_TAG} @@ -70,6 +74,7 @@ pipelines: services: - docker script: + - git submodule update --init --recursive - export START_VERSION='6' - export END_VERSION='7' - export DEFAULT_RELEASE='false' @@ -86,6 +91,7 @@ pipelines: services: - docker script: + - git submodule update --init --recursive - export START_VERSION='6' - export END_VERSION='7.1' - export DEFAULT_RELEASE='true' @@ -102,6 +108,7 @@ pipelines: services: - docker script: + - git submodule update --init --recursive - export START_VERSION='7.1' - export END_VERSION='8' - export DEFAULT_RELEASE='true' @@ -116,9 +123,11 @@ pipelines: name: Update README image: python:3.7-alpine3.9 script: + - apk add --no-cache git + - git submodule update --init --recursive - pip install -q requests - export DOCKER_REPO='atlassian/confluence-server' - - python bin/push-readme.py + - python shared-components/image/push-readme.py pull-requests: '**': - step: @@ -126,9 +135,19 @@ pipelines: services: - docker script: - - pip install -q -r tests/test-requirements.txt + - apk add --no-cache git + - git submodule update --init --recursive + - pip install -q -r shared-components/tests/requirements.txt + - export PYTHONPATH=./shared-components/tests:$PYTHONPATH + - export DOCKERFILE='Dockerfile' + - export DOCKERFILE_VERSION_ARG='CONFLUENCE_VERSION' + - export MAC_PRODUCT_KEY='confluence' - py.test tests/ + - py.test shared-components/tests/ + - export DOCKERFILE='Dockerfile-alpine' + - py.test tests/ + - py.test shared-components/tests/ definitions: services: docker: - memory: 2048 + memory: 3072 diff --git a/entrypoint.py b/entrypoint.py index 22b0cbc..e0bfd40 100755 --- a/entrypoint.py +++ b/entrypoint.py @@ -1,89 +1,19 @@ #!/usr/bin/python3 -import sys -import os -import shutil -import logging -import jinja2 as j2 +from entrypoint_helpers import env, gen_cfg, str2bool, start_app -###################################################################### -# Utils +RUN_USER = env['run_user'] +RUN_GROUP = env['run_group'] +CONFLUENCE_INSTALL_DIR = env['confluence_install_dir'] +CONFLUENCE_HOME = env['confluence_home'] -logging.basicConfig(level=logging.DEBUG) +gen_cfg('server.xml.j2', f'{CONFLUENCE_INSTALL_DIR}/conf/server.xml') +gen_cfg('seraph-config.xml.j2', + f'{CONFLUENCE_INSTALL_DIR}/confluence/WEB-INF/classes/seraph-config.xml') +gen_cfg('confluence-init.properties.j2', + f'{CONFLUENCE_INSTALL_DIR}/confluence/WEB-INF/classes/confluence-init.properties') +gen_cfg('confluence.cfg.xml.j2', f'{CONFLUENCE_HOME}/confluence.cfg.xml', + user=RUN_USER, group=RUN_GROUP, overwrite=False) -def set_perms(path, user, group, 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) - -# Setup Jinja2 for templating -jenv = j2.Environment( - loader=j2.FileSystemLoader('/opt/atlassian/etc/'), - autoescape=j2.select_autoescape(['xml'])) - -def gen_cfg(tmpl, target, env, 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) - with open(target, 'w') as fd: - fd.write(cfg) - set_perms(target, user, group, mode) - - -###################################################################### -# 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() - if k.startswith(('ATL_', 'CONFLUENCE_', 'RUN_', 'CATALINA_'))} - - -###################################################################### -# Generate all configuration files for Confluence - -if os.getuid() == 0: - gen_cfg('server.xml.j2', - f"{env['confluence_install_dir']}/conf/server.xml", env) - - gen_cfg('seraph-config.xml.j2', - f"{env['confluence_install_dir']}/confluence/WEB-INF/classes/seraph-config.xml", env) - - gen_cfg('confluence-init.properties.j2', - f"{env['confluence_install_dir']}/confluence/WEB-INF/classes/confluence-init.properties", env) -else: - logging.warning("Container not started as root. Tomcat, seraph-config.xml, confluence-init.properties boostrapping will be skipped.") - -gen_cfg('confluence.cfg.xml.j2', - f"{env['confluence_home']}/confluence.cfg.xml", env, - user=env['run_user'], group=env['run_group'], mode=0o640, - overwrite=False) - - -###################################################################### -# Start Confluence as the correct user - -start_cmd = f"{env['confluence_install_dir']}/bin/start-confluence.sh" -if os.getuid() == 0: - logging.info(f"User is currently root. Will change directory ownership to {env['run_user']} then downgrade permissions") - set_perms(env['confluence_home'], env['run_user'], env['run_group'], 0o700) - - cmd = '/bin/su' - start_cmd = ' '.join([start_cmd] + sys.argv[1:]) - args = [cmd, env['run_user'], '-c', start_cmd] -else: - cmd = start_cmd - args = [start_cmd] + sys.argv[1:] - -logging.info(f"Running Confluence with command '{cmd}', arguments {args}") -os.execv(cmd, args) +start_app(f'{CONFLUENCE_INSTALL_DIR}/bin/start-confluence.sh -fg', CONFLUENCE_HOME, name='Confluence') diff --git a/hooks/post_push b/hooks/post_push deleted file mode 100644 index e4643e1..0000000 --- a/hooks/post_push +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -set -e - -# Parse image name for repo name -tagStart=$(expr index "$IMAGE_NAME" :) -repoName=${IMAGE_NAME:0:tagStart-1} - -# Tag and push image for each additional tag -for tag in `git tag -l --points-at HEAD`; do - docker tag $IMAGE_NAME ${repoName}:${tag} - docker push ${repoName}:${tag} -done \ No newline at end of file diff --git a/shared-components b/shared-components new file mode 160000 index 0000000..eb1df51 --- /dev/null +++ b/shared-components @@ -0,0 +1 @@ +Subproject commit eb1df51c88ee99c38ce276f85981c07c23319e1d diff --git a/tests/conftest.py b/tests/conftest.py index 79f3425..a36834f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,34 +1,3 @@ import pytest -import docker -import requests - - -MAC_PRODUCT_KEY = 'confluence' -DOCKER_VERSION_ARG = 'CONFLUENCE_VERSION' -DOCKERFILES = ['Dockerfile', 'Dockerfile-alpine'] -IMAGE_NAME = 'confluence-dev' - -# This fixture cleans up running containers whose base image matches IMAGE_NAME 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(IMAGE_NAME): - container.remove(force=True) - - -@pytest.fixture(scope='module', params=DOCKERFILES) -def image(request): - r = requests.get(f'https://marketplace.atlassian.com/rest/2/products/key/{MAC_PRODUCT_KEY}/versions/latest') - version = r.json().get('name') - buildargs = {DOCKER_VERSION_ARG: version} - docker_cli = docker.from_env() - image = docker_cli.images.build(path='.', - tag=f'{IMAGE_NAME}:{request.param.lower()}', - buildargs=buildargs, - dockerfile=request.param, - rm=True)[0] - return image +from fixtures import docker_cli, image, run_user diff --git a/tests/test-requirements.txt b/tests/test-requirements.txt deleted file mode 100644 index c41411a..0000000 --- a/tests/test-requirements.txt +++ /dev/null @@ -1,20 +0,0 @@ -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 -urllib3==1.25.3 -wcwidth==0.1.7 -websocket-client==0.56.0 -zipp==0.5.2 -testinfra==3.0.6 diff --git a/tests/test_image.py b/tests/test_image.py index 32628c0..9e71134 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -1,62 +1,22 @@ import pytest -import io -import tarfile -import testinfra -import time -import xml.etree.ElementTree as etree -import requests +from helpers import get_app_home, get_app_install_dir, get_bootstrap_proc, get_procs, \ + parse_properties, parse_xml, run_image, wait_for_http_response, wait_for_proc -CONF_INSTALL = '/opt/atlassian/confluence' -CONF_HOME = '/var/atlassian/application-data/confluence' -CONF_SHARED_HOME = '/media/atl/confluence/shared-home' -# 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 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") - - -###################################################################### -# Tests - -def test_jvm_args(docker_cli, image): +def test_jvm_args(docker_cli, image, run_user): environment = { 'JVM_MINIMUM_MEMORY': '383m', 'JVM_MAXIMUM_MEMORY': '2047m', 'JVM_SUPPORT_RECOMMENDED_ARGS': '-verbose:gc', } - container = run_image(docker_cli, image, environment=environment) - jvm = wait_for_proc(container, "org.apache.catalina.startup.Bootstrap") - + container = run_image(docker_cli, image, user=run_user, environment=environment) + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) + + procs_list = get_procs(container) + jvm = [proc for proc in procs_list if get_bootstrap_proc(container) in proc][0] + assert f'-Xms{environment.get("JVM_MINIMUM_MEMORY")}' in jvm assert f'-Xmx{environment.get("JVM_MAXIMUM_MEMORY")}' in jvm assert environment.get('JVM_SUPPORT_RECOMMENDED_ARGS') in jvm @@ -65,37 +25,27 @@ def test_jvm_args(docker_cli, image): def test_install_permissions(docker_cli, image): container = run_image(docker_cli, image) - assert container.file(f'{CONF_INSTALL}').user == 'root' + assert container.file(f'{get_app_install_dir(container)}').user == 'root' for d in ['logs', 'work', 'temp']: - path = f'{CONF_INSTALL}/{d}' + path = f'{get_app_install_dir(container)}/{d}' assert container.file(path).user == 'confluence' -def test_first_run_state(docker_cli, image): +def test_first_run_state(docker_cli, image, run_user): PORT = 8090 - container = run_image(docker_cli, image, ports={PORT: PORT}) - jvm = wait_for_proc(container, "org.apache.catalina.startup.Bootstrap") - - for i in range(20): - try: - r = requests.get(f'http://localhost:{PORT}/status') - except requests.exceptions.ConnectionError: - pass - else: - if r.status_code == 200: - state = r.json().get('state') - assert state in ('STARTING', 'FIRST_RUN') - return - time.sleep(1) - raise TimeoutError + URL = f'http://localhost:{PORT}/status' + + container = run_image(docker_cli, image, user=run_user, ports={PORT: PORT}) + + wait_for_http_response(URL, expected_status=200, expected_state=('STARTING', 'FIRST_RUN')) def test_server_xml_defaults(docker_cli, image): container = run_image(docker_cli, image) - _jvm = wait_for_proc(container, "org.apache.catalina.startup.Bootstrap") + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) - xml = etree.fromstring(container.file(f'{CONF_INSTALL}/conf/server.xml').content) + xml = parse_xml(container, f'{get_app_install_dir(container)}/conf/server.xml') connector = xml.find('.//Connector') context = xml.find('.//Context') @@ -111,6 +61,7 @@ def test_server_xml_defaults(docker_cli, image): assert connector.get('proxyName') == '' assert connector.get('proxyPort') == '' + def test_server_xml_catalina_fallback(docker_cli, image): environment = { 'CATALINA_CONNECTOR_PROXYNAME': 'PROXYNAME', @@ -120,9 +71,9 @@ def test_server_xml_catalina_fallback(docker_cli, image): 'CATALINA_CONTEXT_PATH': 'CONTEXT' } container = run_image(docker_cli, image, environment=environment) - _jvm = wait_for_proc(container, "org.apache.catalina.startup.Bootstrap") + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) - xml = etree.fromstring(container.file(f'{CONF_INSTALL}/conf/server.xml').content) + xml = parse_xml(container, f'{get_app_install_dir(container)}/conf/server.xml') connector = xml.find('.//Connector') context = xml.find('.//Context') @@ -132,6 +83,7 @@ def test_server_xml_catalina_fallback(docker_cli, image): assert connector.get('secure') == 'SECURE' assert context.get('path') == 'CONTEXT' + def test_server_xml_params(docker_cli, image): environment = { 'ATL_TOMCAT_MGMT_PORT': '8006', @@ -149,9 +101,9 @@ def test_server_xml_params(docker_cli, image): 'ATL_TOMCAT_CONTEXTPATH': '/myconf', } container = run_image(docker_cli, image, environment=environment) - _jvm = wait_for_proc(container, "org.apache.catalina.startup.Bootstrap") + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) - xml = etree.fromstring(container.file(f'{CONF_INSTALL}/conf/server.xml').content) + xml = parse_xml(container, f'{get_app_install_dir(container)}/conf/server.xml') connector = xml.find('.//Connector') context = xml.find('.//Context') @@ -174,49 +126,49 @@ def test_server_xml_params(docker_cli, image): def test_seraph_defaults(docker_cli, image): container = run_image(docker_cli, image) - wait_for_file(container, f"{CONF_INSTALL}/confluence/WEB-INF/classes/seraph-config.xml") + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) - xml = etree.fromstring(container.file(f'{CONF_INSTALL}/confluence/WEB-INF/classes/seraph-config.xml').content) - #param = xml.findall('//param-name[text()="autologin.cookie.age"]') == [] + xml = parse_xml(container, f'{get_app_install_dir(container)}/confluence/WEB-INF/classes/seraph-config.xml') param = xml.findall('.//param-name[.="autologin.cookie.age"]') == [] + def test_seraph_login_set(docker_cli, image): container = run_image(docker_cli, image, environment={"ATL_AUTOLOGIN_COOKIE_AGE": "TEST_VAL"}) - wait_for_file(container, f"{CONF_INSTALL}/confluence/WEB-INF/classes/seraph-config.xml") + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) - xml = etree.fromstring(container.file(f'{CONF_INSTALL}/confluence/WEB-INF/classes/seraph-config.xml').content) + xml = parse_xml(container, f'{get_app_install_dir(container)}/confluence/WEB-INF/classes/seraph-config.xml') assert xml.findall('.//param-value[.="TEST_VAL"]')[0].text == "TEST_VAL" def test_conf_init_set(docker_cli, image): container = run_image(docker_cli, image, environment={"CONFLUENCE_HOME": "/tmp/"}) - wait_for_file(container, f"{CONF_INSTALL}/confluence/WEB-INF/classes/confluence-init.properties") + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) - init = container.file(f'{CONF_INSTALL}/confluence/WEB-INF/classes/confluence-init.properties') + init = container.file(f'{get_app_install_dir(container)}/confluence/WEB-INF/classes/confluence-init.properties') assert init.contains("confluence.home = /tmp/") def test_confluence_xml_default(docker_cli, image): container = run_image(docker_cli, image) - wait_for_file(container, f"{CONF_INSTALL}/confluence/WEB-INF/classes/confluence-init.properties") - #_jvm = wait_for_proc(container, "org.apache.catalina.startup.Bootstrap") + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) - xml = etree.fromstring(container.file(f'{CONF_HOME}/confluence.cfg.xml').content) + xml = parse_xml(container, f'{get_app_home(container)}/confluence.cfg.xml') assert xml.findall('.//buildNumber')[0].text == "0" assert xml.findall('.//property[@name="hibernate.connection.url"]') == [] assert xml.findall('.//property[@name="confluence.cluster.home"]') == [] -def test_confluence_xml_postgres(docker_cli, image): + +def test_confluence_xml_postgres(docker_cli, image, run_user): environment = { 'ATL_DB_TYPE': 'postgresql', 'ATL_JDBC_URL': 'atl_jdbc_url', 'ATL_JDBC_USER': 'atl_jdbc_user', 'ATL_JDBC_PASSWORD': 'atl_jdbc_password' } - container = run_image(docker_cli, image, environment=environment) - wait_for_file(container, f"{CONF_INSTALL}/confluence/WEB-INF/classes/confluence-init.properties") - - xml = etree.fromstring(container.file(f'{CONF_HOME}/confluence.cfg.xml').content) + container = run_image(docker_cli, image, user=run_user, environment=environment) + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) + + xml = parse_xml(container, f'{get_app_home(container)}/confluence.cfg.xml') assert xml.findall('.//property[@name="hibernate.connection.url"]')[0].text == "atl_jdbc_url" assert xml.findall('.//property[@name="hibernate.connection.username"]')[0].text == "atl_jdbc_user" assert xml.findall('.//property[@name="hibernate.connection.password"]')[0].text == "atl_jdbc_password" @@ -233,7 +185,8 @@ def test_confluence_xml_postgres(docker_cli, image): assert xml.findall('.//property[@name="hibernate.c3p0.acquire_increment"]')[0].text == "1" assert xml.findall('.//property[@name="hibernate.c3p0.preferredTestQuery"]')[0].text == "select 1" -def test_confluence_xml_postgres_all_set(docker_cli, image): + +def test_confluence_xml_postgres_all_set(docker_cli, image, run_user): environment = { 'ATL_DB_TYPE': 'postgresql', 'ATL_JDBC_URL': 'atl_jdbc_url', @@ -248,10 +201,10 @@ def test_confluence_xml_postgres_all_set(docker_cli, image): 'ATL_DB_ACQUIREINCREMENT': 'x1', 'ATL_DB_VALIDATIONQUERY': 'xselect 1' } - container = run_image(docker_cli, image, environment=environment) - wait_for_file(container, f"{CONF_HOME}/confluence.cfg.xml") + container = run_image(docker_cli, image, user=run_user, environment=environment) + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) - xml = etree.fromstring(container.file(f'{CONF_HOME}/confluence.cfg.xml').content) + xml = parse_xml(container, f'{get_app_home(container)}/confluence.cfg.xml') assert xml.findall('.//property[@name="hibernate.connection.driver_class"]')[0].text == "org.postgresql.Driver" assert xml.findall('.//property[@name="hibernate.dialect"]')[0].text == "com.atlassian.confluence.impl.hibernate.dialect.PostgreSQLDialect" assert xml.findall('.//property[@name="hibernate.c3p0.min_size"]')[0].text == "x20" @@ -264,7 +217,7 @@ def test_confluence_xml_postgres_all_set(docker_cli, image): assert xml.findall('.//property[@name="hibernate.c3p0.preferredTestQuery"]')[0].text == "xselect 1" -def test_confluence_xml_cluster_aws(docker_cli, image): +def test_confluence_xml_cluster_aws(docker_cli, image, run_user): environment = { 'ATL_CLUSTER_TYPE': 'aws', 'ATL_HAZELCAST_NETWORK_AWS_IAM_ROLE': 'atl_hazelcast_network_aws_iam_role', @@ -275,9 +228,10 @@ def test_confluence_xml_cluster_aws(docker_cli, image): 'ATL_CLUSTER_NAME': 'atl_cluster_name', 'ATL_CLUSTER_TTL': 'atl_cluster_ttl' } - container = run_image(docker_cli, image, environment=environment) - wait_for_file(container, f"{CONF_HOME}/confluence.cfg.xml") - xml = etree.fromstring(container.file(f'{CONF_HOME}/confluence.cfg.xml').content) + container = run_image(docker_cli, image, user=run_user, environment=environment) + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) + + xml = parse_xml(container, f'{get_app_home(container)}/confluence.cfg.xml') assert xml.findall('.//property[@name="confluence.cluster"]')[0].text == "true" assert xml.findall('.//property[@name="confluence.cluster.join.type"]')[0].text == "aws" @@ -290,16 +244,18 @@ def test_confluence_xml_cluster_aws(docker_cli, image): assert xml.findall('.//property[@name="confluence.cluster.name"]')[0].text == "atl_cluster_name" assert xml.findall('.//property[@name="confluence.cluster.ttl"]')[0].text == "atl_cluster_ttl" -def test_confluence_xml_cluster_multicast(docker_cli, image): + +def test_confluence_xml_cluster_multicast(docker_cli, image, run_user): environment = { 'ATL_CLUSTER_TYPE': 'multicast', 'ATL_CLUSTER_NAME': 'atl_cluster_name', 'ATL_CLUSTER_TTL': 'atl_cluster_ttl', 'ATL_CLUSTER_ADDRESS': '99.99.99.99' } - container = run_image(docker_cli, image, environment=environment) - wait_for_file(container, f"{CONF_HOME}/confluence.cfg.xml") - xml = etree.fromstring(container.file(f'{CONF_HOME}/confluence.cfg.xml').content) + container = run_image(docker_cli, image, user=run_user, environment=environment) + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) + + xml = parse_xml(container, f'{get_app_home(container)}/confluence.cfg.xml') assert xml.findall('.//property[@name="confluence.cluster"]')[0].text == "true" assert xml.findall('.//property[@name="confluence.cluster.join.type"]')[0].text == "multicast" @@ -307,27 +263,31 @@ def test_confluence_xml_cluster_multicast(docker_cli, image): assert xml.findall('.//property[@name="confluence.cluster.ttl"]')[0].text == "atl_cluster_ttl" assert xml.findall('.//property[@name="confluence.cluster.address"]')[0].text == "99.99.99.99" -def test_confluence_xml_cluster_tcp(docker_cli, image): + +def test_confluence_xml_cluster_tcp(docker_cli, image, run_user): environment = { 'ATL_CLUSTER_TYPE': 'tcp_ip', 'ATL_CLUSTER_PEERS': '1.1.1.1,99.99.99.99', 'ATL_CLUSTER_NAME': 'atl_cluster_name', } - container = run_image(docker_cli, image, environment=environment) - wait_for_file(container, f"{CONF_HOME}/confluence.cfg.xml") - xml = etree.fromstring(container.file(f'{CONF_HOME}/confluence.cfg.xml').content) + container = run_image(docker_cli, image, user=run_user, environment=environment) + _jvm = wait_for_proc(container, get_bootstrap_proc(container)) + + xml = parse_xml(container, f'{get_app_home(container)}/confluence.cfg.xml') assert xml.findall('.//property[@name="confluence.cluster"]')[0].text == "true" assert xml.findall('.//property[@name="confluence.cluster.join.type"]')[0].text == "tcp_ip" assert xml.findall('.//property[@name="confluence.cluster.name"]')[0].text == "atl_cluster_name" assert xml.findall('.//property[@name="confluence.cluster.peers"]')[0].text == "1.1.1.1,99.99.99.99" + def test_java_in_run_user_path(docker_cli, image): RUN_USER = 'confluence' container = run_image(docker_cli, image) proc = container.run(f'su -c "which java" {RUN_USER}') assert len(proc.stdout) > 0 + def test_non_root_user(docker_cli, image): RUN_UID = 2002 RUN_GID = 2002