mirror of
https://bitbucket.org/atlassian-docker/docker-atlassian-confluence-server.git
synced 2024-08-30 18:22:16 +00:00
Merged in DCKUBE-136-RunSmoketestsInReleasePipeline (pull request #83)
DCKUBE-135, DCKUBE-136, DCKUBE_137, and DCKUBE-138: Add Smoke tests in Confluence Pipeline This PR contains code change for three tickets which are related and should merge to master together. KUBEDCKUBE-135: Applied security scan to bitbucket pipeline for branch builds in confluence - for releases, the test script will run by run.py for releases and for branch builds and custom releases will directly runs snyk scanner Created a smoke testing suite via REST and included these scenarios: Create a space, Create a page, Search for the page, View page, Add attachments, Delete the page, and Delete the space KUBEDCKUBE-136: Added a separated docker for confluence to based on the docker image to copy confluence home directory Injected target confluence image to Dockerfile set the number of concurrent builds to one in pipeline clean docker-compose before start and force to recreate the containers Modified the script in order to install netcat-openbsd using apkfor Alpine (apt-getis not available in Alpine) Replaced colfuence-home directory and postgres scripts with confluence 6.0.1 compatible to avoid downgrade version in release images Increased database connection numbers to 125 Addressed some review points, replaced the confluence home directory and sql with version 6.0.1 Addressed a review points, renamed CONFLUENCE_USER to CONFLUENCE_ADMIN divided pipeline into batches to avoid pipeline timeout KUBEDCKUBE-137: Run smoketests in branch builds after each commit Completed smoketests and also addressed some review points KUBEDCKUBE-138: Added development document Approved-by: Adam Brokes
This commit is contained in:
parent
42592fab7e
commit
2a28ea5182
94
DEVELOPMENT.md
Normal file
94
DEVELOPMENT.md
Normal file
@ -0,0 +1,94 @@
|
||||
# Developing Docker images
|
||||
|
||||
## Setting up for development
|
||||
|
||||
After cloning the repository you will need to clone the submodule:
|
||||
|
||||
```
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
You should also add the local githooks, which include checks for the generated
|
||||
Bitbucket Pipelines configuration. This can be done with:
|
||||
|
||||
```
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The repository contains a smoke-test suite run on branch/PR builds and as part
|
||||
of the release process. It uses `docker-compose` to run the image in production
|
||||
mode with predefined database, and then run a suite of basic tests. *NOTE*: The
|
||||
tests are not intended to test every aspect of the product, but to ensure basic
|
||||
functionality works to a degree that we can be confident that there are no
|
||||
regressions. See [func-tests/smoketests/tests/](func-tests/smoketests/tests/) for the actual tests run.
|
||||
|
||||
The default database was generated with the default sample project, but has had
|
||||
the license elided for security reasons. See below for how to inject it.
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
To run the functional testing, you are required to define several variables.
|
||||
|
||||
```
|
||||
# String with full Confluence DC license, don't forget the quotes as the license
|
||||
can contain special characters that would render the license unusable
|
||||
CONFLUENCE_TEST_LICENSE='license_string'
|
||||
|
||||
# The user and passwords used to access the product. These are stored in lastpass.
|
||||
CONFLUENCE_ADMIN='xxx'
|
||||
CONFLUENCE_ADMIN_PWD='xxx'
|
||||
```
|
||||
|
||||
### Run the smoke test functional suite
|
||||
|
||||
This will build a local image based on the latest Confluence version and run the
|
||||
func-tests against it:
|
||||
|
||||
```
|
||||
export CONFLUENCE_VERSION=`curl -s https://marketplace.atlassian.com/rest/2/products/key/confluence/versions/latest | jq -r .name`
|
||||
docker build --build-arg CONFLUENCE_VERSION=${CONFLUENCE_VERSION} -t confluence-test-image .
|
||||
docker-compose build --build-arg TEST_TARGET_IMAGE=confluence-test-image ./func-tests/docker-compose.yaml
|
||||
./func-tests/run-functests confluence-test-image
|
||||
```
|
||||
|
||||
### Develop tests
|
||||
|
||||
Make sure CONFLUENCE_TEST_LICENSE, CONFLUENCE_ADMIN, and CONFLUENCE_ADMIN_PWD environment
|
||||
variables already are set. Extract confluence home directory and inject the license:
|
||||
|
||||
```
|
||||
cd func-tests
|
||||
unzip -o confluence-home-6.0.1.zip -d confluence
|
||||
TEST_TARGET_IMAGE='xxx' ./confluence/inject-license
|
||||
```
|
||||
|
||||
Run the smoke tests
|
||||
|
||||
```
|
||||
cd func-tests
|
||||
TEST_TARGET_IMAGE='xxx' docker-compose up --force-recreate --always-recreate-deps --abort-on-container-exit --exit-code-from smoketests
|
||||
```
|
||||
|
||||
### Release process
|
||||
|
||||
Releases occur automatically; see [bitbucket-pipelines.yml](bitbucket-pipelines.yml).
|
||||
Due to the large amount of images that are built and tested the pipelines file
|
||||
is generated from a template that parallelises the builds. It includes a
|
||||
self-check for out-of-date pipelines config. To avoid committing stale config it
|
||||
is recommended you add the supplied pre-commit hook; see the setup section above.
|
||||
|
||||
It should be noted that a change to this repository will result in all published
|
||||
images being regenerated with the latest version of the
|
||||
[Dockerfile](Dockerfile). As part of the release process the following happens:
|
||||
|
||||
* A [Snyk](https://snyk.io) scan is run against the generated container image.
|
||||
* The image dependencies are registered with Snyk for periodic scanning.
|
||||
* The above func-test suite is run against the image.
|
||||
|
||||
This is all performed by the
|
||||
[docker-release-maker](https://bitbucket.org/atlassian-docker/docker-release-maker/)
|
||||
tool/image. See the
|
||||
[README](https://bitbucket.org/atlassian-docker/docker-release-maker/src/master/README.md)
|
||||
in that repository for more information.
|
File diff suppressed because it is too large
Load Diff
BIN
func-tests/confluence-home-6.0.1.zip
Normal file
BIN
func-tests/confluence-home-6.0.1.zip
Normal file
Binary file not shown.
4
func-tests/confluence/Dockerfile.tmpl
Normal file
4
func-tests/confluence/Dockerfile.tmpl
Normal file
@ -0,0 +1,4 @@
|
||||
FROM INJECT_BASE_IMAGE_HERE
|
||||
|
||||
COPY confluence-home/ /var/atlassian/application-data/confluence/
|
||||
RUN chown -R confluence.confluence /var/atlassian/application-data/confluence
|
6
func-tests/confluence/inject-license
Executable file
6
func-tests/confluence/inject-license
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# NOTE: This expects the license to be available in $CONFLUENCE_TEST_LICENSE variable
|
||||
CONFLUENCE_TEST_LICENSE=`echo ${CONFLUENCE_TEST_LICENSE} | tr -d '\n \t'`
|
||||
|
||||
sed "s~INJECT_LICENSE_HERE~${CONFLUENCE_TEST_LICENSE}~" confluence/confluence-home/confluence.cfg.xml.tmpl > confluence/confluence-home/confluence.cfg.xml
|
||||
sed "s~INJECT_BASE_IMAGE_HERE~${TEST_TARGET_IMAGE}~" confluence/Dockerfile.tmpl > confluence/Dockerfile
|
44
func-tests/docker-compose.yml
Normal file
44
func-tests/docker-compose.yml
Normal file
@ -0,0 +1,44 @@
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
build:
|
||||
context: ./postgres
|
||||
ports:
|
||||
- '5432:5432'
|
||||
environment:
|
||||
- 'POSTGRES_DB=confluence'
|
||||
- 'POSTGRES_USER=confluence'
|
||||
- 'POSTGRES_PASSWORD=confluence'
|
||||
- 'POSTGRES_ENCODING=utf-8'
|
||||
- 'POSTGRES_COLLATE=utf-8'
|
||||
- 'POSTGRES_COLLATE_TYPE=utf-8'
|
||||
- "LANG=utf-8"
|
||||
|
||||
confluence:
|
||||
build:
|
||||
context: ./confluence
|
||||
depends_on:
|
||||
- postgresql
|
||||
ports:
|
||||
- '8090:8090'
|
||||
command: >
|
||||
bash -c '
|
||||
if grep -oh 'Alpine' < /etc/os-release ; then apk update && apk add netcat-openbsd; else apt-get update -y && apt-get install -y netcat; fi &&
|
||||
/opt/atlassian/support/waitport postgresql 5432 &&
|
||||
chown -R confluence.confluence /var/atlassian/application-data/confluence/ &&
|
||||
/entrypoint.py
|
||||
'
|
||||
|
||||
smoketests:
|
||||
build:
|
||||
context: ./smoketests/
|
||||
environment:
|
||||
- CONFLUENCE_BASE_URL=http://confluence:8090
|
||||
- CONFLUENCE_ADMIN=${CONFLUENCE_ADMIN}
|
||||
- CONFLUENCE_ADMIN_PWD=${CONFLUENCE_ADMIN_PWD}
|
||||
command: >
|
||||
bash -c '
|
||||
./bin/confluence-wait &&
|
||||
pytest -v
|
||||
'
|
3
func-tests/postgres/Dockerfile
Normal file
3
func-tests/postgres/Dockerfile
Normal file
@ -0,0 +1,3 @@
|
||||
FROM postgres:10.16-alpine
|
||||
|
||||
COPY confluence-6.0.1.sql /docker-entrypoint-initdb.d/confluence.sql
|
13594
func-tests/postgres/confluence-6.0.1.sql
Normal file
13594
func-tests/postgres/confluence-6.0.1.sql
Normal file
File diff suppressed because one or more lines are too long
24
func-tests/run-functests
Executable file
24
func-tests/run-functests
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
latest_image=$(docker images --format "{{.ID}} {{.CreatedAt}}" | sort -rk 2 | awk 'NR==1{print $1}')
|
||||
|
||||
TEST_TARGET_IMAGE=${1:-$latest_image}
|
||||
DIR=$(dirname "$0")
|
||||
export TEST_TARGET_IMAGE
|
||||
export DIR
|
||||
|
||||
# Assumes this script is in the func-tests base dir
|
||||
cd "$DIR" || exit
|
||||
|
||||
if [ -z "$CONFLUENCE_TEST_LICENSE" ]; then
|
||||
echo "You need to define CONFLUENCE_TEST_LICENSE env variable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
unzip -o confluence-home-6.0.1.zip -d confluence
|
||||
# NOTE: This expects the license to have been injected
|
||||
sh ./confluence/inject-license
|
||||
|
||||
docker-compose rm -f && \
|
||||
docker-compose up --force-recreate --always-recreate-deps --abort-on-container-exit --exit-code-from smoketests
|
||||
|
10
func-tests/smoketests/Dockerfile
Normal file
10
func-tests/smoketests/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM python:3.9
|
||||
|
||||
RUN apt-get update
|
||||
RUN pip install pytest
|
||||
RUN pip install requests
|
||||
|
||||
RUN mkdir /opt/test
|
||||
COPY . /opt/test
|
||||
WORKDIR /opt/test
|
||||
|
27
func-tests/smoketests/bin/confluence-wait
Executable file
27
func-tests/smoketests/bin/confluence-wait
Executable file
@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
max=60
|
||||
sleep_interval=5
|
||||
|
||||
echo "Waiting for Confluence to come up at $CONFLUENCE_BASE_URL..."
|
||||
|
||||
for i in $(seq $max); do
|
||||
# Confluence emits `302` during startup, `200` when ready:
|
||||
status=$(curl -u "$CONFLUENCE_ADMIN":"$CONFLUENCE_ADMIN_PWD" -s -o /dev/null -w "%{http_code}" "$CONFLUENCE_BASE_URL"/status)
|
||||
echo Confluence returned "$status"
|
||||
if [[ $status == "200" ]]; then
|
||||
echo OK
|
||||
echo Confluence is up and running
|
||||
exit 0
|
||||
elif [[ $status -ge "500" ]]; then
|
||||
echo ERROR
|
||||
echo Confluence failed to start due to a server error
|
||||
exit 1
|
||||
fi
|
||||
|
||||
/bin/sleep $sleep_interval
|
||||
|
||||
done
|
||||
|
||||
echo Confluence failed to startup within $((max * sleep_interval)) seconds
|
||||
exit 1
|
22
func-tests/smoketests/tests/file_helper.py
Normal file
22
func-tests/smoketests/tests/file_helper.py
Normal file
@ -0,0 +1,22 @@
|
||||
import os
|
||||
import mimetypes
|
||||
import tempfile
|
||||
|
||||
|
||||
def create_temp_files():
|
||||
f = tempfile.NamedTemporaryFile(delete=False)
|
||||
f.write(b"This temporary file is created by file_helper!\n")
|
||||
f.close()
|
||||
content_type, encoding = mimetypes.guess_type(f.name)
|
||||
if content_type is None:
|
||||
content_type = 'multipart/form-data'
|
||||
files = {'file': (f.name, open(f.name, 'rb'), content_type)}
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def remove_temp_files(files):
|
||||
if(len(files['file']) > 1):
|
||||
os.remove(files['file'][0])
|
||||
return not os.path.exists(files['file'][0])
|
||||
return True
|
211
func-tests/smoketests/tests/test_smoketests.py
Normal file
211
func-tests/smoketests/tests/test_smoketests.py
Normal file
@ -0,0 +1,211 @@
|
||||
import pytest
|
||||
import requests
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
from requests.auth import HTTPBasicAuth
|
||||
import file_helper
|
||||
|
||||
|
||||
def test_create_space(space_key):
|
||||
url = f"{REST_API_URL}/space"
|
||||
|
||||
data = {
|
||||
"key": space_key,
|
||||
"name": "Test new space",
|
||||
"description": {
|
||||
"plain": {
|
||||
"value": "test space generated by api for smoketest",
|
||||
"representation": "plain"
|
||||
}
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
r = requests.post(url, data=json.dumps(data), headers=headers, auth=auth)
|
||||
assert r.status_code == 200, \
|
||||
f'failed to create space {space_key}! status:{r.status_code}'
|
||||
|
||||
|
||||
def test_create_content(space_key, title, content, content_id):
|
||||
url = f"{REST_API_URL}/content"
|
||||
data = {
|
||||
"type": "page",
|
||||
"status": "current",
|
||||
"title": title,
|
||||
"space": {"key": space_key},
|
||||
"representation": "storage",
|
||||
"body":
|
||||
{
|
||||
"storage":
|
||||
{
|
||||
"value": f"<p>{content}</p>",
|
||||
"representation": "storage"
|
||||
}
|
||||
}
|
||||
}
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
r = requests.post(url, data=json.dumps(data), headers=headers, auth=auth)
|
||||
assert r.status_code == 200, \
|
||||
f'failed to create page "{title}"! status:{r.status_code}'
|
||||
result = json.loads(r.text)
|
||||
content_id['val'] = result['id']
|
||||
|
||||
|
||||
def test_search_content(content):
|
||||
url = f"{REST_API_URL}/content/search?cql=text~'{content}'"
|
||||
attempts = 0
|
||||
while True:
|
||||
r = requests.get(url, auth=auth)
|
||||
if r.status_code == 200:
|
||||
if len(json.loads(r.text)['results']) > 0:
|
||||
break
|
||||
attempts += 1
|
||||
if attempts > max_wait_for_response:
|
||||
break
|
||||
# Add more delay if index still updating and no result is returned
|
||||
time.sleep(1)
|
||||
|
||||
assert r.status_code == 200, \
|
||||
f'failed to search content! status:{r.status_code}'
|
||||
assert len(json.loads(r.text)['results']) > 0, \
|
||||
f'failed to find the search term!'
|
||||
|
||||
|
||||
def test_view_content(content_id):
|
||||
url = f"{REST_API_URL}/content/{content_id['val']}?expand=body.view"
|
||||
r = requests.get(url, auth=auth)
|
||||
assert r.status_code == 200, \
|
||||
f'failed to view the page! status:{r.status_code}'
|
||||
assert len(json.loads(r.text)['body']['view']['value']) > 0, \
|
||||
"no content is loaded"
|
||||
|
||||
|
||||
def test_edit_content(content_id):
|
||||
url = f"{REST_API_URL}/content/{content_id['val']}"
|
||||
data = {
|
||||
"version": {
|
||||
"number": 2
|
||||
},
|
||||
"type": "page",
|
||||
"title": "New Title",
|
||||
"body":
|
||||
{
|
||||
"storage":
|
||||
{
|
||||
"value": "<p>This page is edited for smoketest!</p>",
|
||||
"representation": "storage"
|
||||
}
|
||||
}
|
||||
}
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
r = requests.put(url, data=json.dumps(data), headers=headers, auth=auth)
|
||||
assert r.status_code == 200, \
|
||||
f'failed to edit page "{title}"! status:{r.status_code}'
|
||||
|
||||
|
||||
def test_add_attachments(content_id, tempfile):
|
||||
url = f"{REST_API_URL}/content/{content_id['val']}/child/attachment"
|
||||
headers = {'X-Atlassian-Token': 'no-check'}
|
||||
# no content-type here!
|
||||
|
||||
r = requests.post(url, headers=headers, files=tempfile, auth=auth)
|
||||
assert r.status_code == 200, \
|
||||
f'failed to upload the attachment! status:{r.status_code}'
|
||||
|
||||
|
||||
def test_retrieve_attachment(content_id, tempfile):
|
||||
url = f"{REST_API_URL}/content/{content_id['val']}/child/attachment"
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
r = requests.get(url, headers=headers, auth=auth)
|
||||
assert r.status_code == 200,\
|
||||
f'failed to find the attachments! status:{r.status_code}'
|
||||
result = json.loads(r.text)
|
||||
|
||||
assert tempfile['file'][0].endswith(result['results'][0]['title']), \
|
||||
"The attachment file is not found!"
|
||||
downloadurl = f"{BASE_URL}{result['results'][0]['_links']['download']}"
|
||||
attempts = 0
|
||||
while True:
|
||||
r = requests.get(downloadurl, headers=headers, auth=auth)
|
||||
assert r.status_code == 200, "Unable to download the attachment!"
|
||||
file_content = open(tempfile['file'][0], 'r').read()
|
||||
if file_content == r.text or attempts > max_wait_for_response:
|
||||
break
|
||||
# makes more delay to upload from last test get in place
|
||||
attempts += 1
|
||||
time.sleep(1)
|
||||
assert file_content == r.text,\
|
||||
"The content of downloaded file is not match with the original file"
|
||||
|
||||
|
||||
def test_delete_content(content_id):
|
||||
url = f"{REST_API_URL}/content/{content_id['val']}"
|
||||
r = requests.delete(url, auth=auth)
|
||||
# returns 200(trash) or 204(purge) on success and 404 or 409 if failed
|
||||
assert r.status_code == 200 or r.status_code == 204,\
|
||||
f'failed to delete the page! status:{r.status_code}'
|
||||
confirm_status_404(url)
|
||||
|
||||
|
||||
def test_delete_space(space_key):
|
||||
url = f"{REST_API_URL}/space/{space_key}"
|
||||
r = requests.delete(url, auth=auth)
|
||||
# returns 202 on success and 404 if failed
|
||||
assert r.status_code == 202, \
|
||||
f'failed to delete the space! status:{r.status_code}'
|
||||
confirm_status_404(url)
|
||||
|
||||
|
||||
def confirm_status_404(url):
|
||||
attempts = 0
|
||||
while True:
|
||||
r = requests.get(url, auth=auth)
|
||||
if r.status_code == 404 or attempts >= max_wait_for_response:
|
||||
break
|
||||
attempts += 1
|
||||
time.sleep(1)
|
||||
assert r.status_code == 404, f'failed to confirm status code 404!'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def space_key():
|
||||
return 'testspace01'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def title():
|
||||
return 'second page'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def content():
|
||||
return 'This page is created for smoketest!'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tempfile():
|
||||
return files
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def content_id():
|
||||
return os.environ.get('GENERATED_CONTENT_ID')
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def content_id():
|
||||
return {'val': 0}
|
||||
|
||||
|
||||
# global variablea
|
||||
BASE_URL = os.environ.get('CONFLUENCE_BASE_URL', "http://localhost:8090")
|
||||
REST_API_URL = f"{BASE_URL}/rest/api"
|
||||
password = os.environ.get('CONFLUENCE_ADMIN_PWD', 'admin')
|
||||
user = os.environ.get('CONFLUENCE_ADMIN', 'admin')
|
||||
auth = HTTPBasicAuth(user, password)
|
||||
max_wait_for_response = 30
|
||||
files = file_helper.create_temp_files()
|
8
func-tests/stop-functests
Executable file
8
func-tests/stop-functests
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
DIR=$(dirname "$0")
|
||||
export DIR
|
||||
|
||||
cd "$DIR" || exit
|
||||
|
||||
docker-compose down
|
Loading…
Reference in New Issue
Block a user