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:
Nasser Ghazali-Beiklar 2021-03-12 03:30:51 +00:00
parent 42592fab7e
commit 2a28ea5182
14 changed files with 14965 additions and 61 deletions

94
DEVELOPMENT.md Normal file
View 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

Binary file not shown.

View 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

View 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

View 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
'

View File

@ -0,0 +1,3 @@
FROM postgres:10.16-alpine
COPY confluence-6.0.1.sql /docker-entrypoint-initdb.d/confluence.sql

File diff suppressed because one or more lines are too long

24
func-tests/run-functests Executable file
View 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

View 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

View 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

View 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

View 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
View File

@ -0,0 +1,8 @@
#!/bin/bash
DIR=$(dirname "$0")
export DIR
cd "$DIR" || exit
docker-compose down