From 584d4cdc579d864f1bddf63b4b97c213ead77117 Mon Sep 17 00:00:00 2001 From: Frank Escobar Date: Thu, 14 Jan 2021 12:22:34 +0000 Subject: [PATCH 1/5] Updating Allure to 2.13.8 --- .travis.yml | 2 +- README.md | 12 ++++++------ docker-compose-dev.yml | 2 +- docker-custom/Dockerfile.bionic-custom | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2fddefd..4f50e1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ language: bash env: global: - - ALLURE_RELEASE=2.13.7 + - ALLURE_RELEASE=2.13.8 - TARGET=frankescobar/allure-docker-service - QEMU_VERSION=v4.0.0 matrix: diff --git a/README.md b/README.md index bddd5dd..3e2537d 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,9 @@ The following table shows the provided Manifest Lists. | **Tag** | **allure-docker-service Base Image** | |----------------------------------------|---------------------------------------------------| -| latest, 2.13.7 | frankescobar/allure-docker-service:2.13.7-amd64 | -| | frankescobar/allure-docker-service:2.13.7-arm32v7 | -| | frankescobar/allure-docker-service:2.13.7-arm64v8 | +| latest, 2.13.8 | frankescobar/allure-docker-service:2.13.8-amd64 | +| | frankescobar/allure-docker-service:2.13.8-arm32v7 | +| | frankescobar/allure-docker-service:2.13.8-arm64v8 | ## USAGE ### Generate Allure Results @@ -706,7 +706,7 @@ You can switch the version container using `frankescobar/allure-docker-service:$ Docker Compose example: ```sh allure: - image: "frankescobar/allure-docker-service:2.13.7" + image: "frankescobar/allure-docker-service:2.13.8" ``` or using latest version: @@ -1314,7 +1314,7 @@ If you want to use docker without sudo, read following links: ### Build image ```sh -docker build -t allure-release -f docker-custom/Dockerfile.bionic-custom --build-arg ALLURE_RELEASE=2.13.7 . +docker build -t allure-release -f docker-custom/Dockerfile.bionic-custom --build-arg ALLURE_RELEASE=2.13.8 . ``` ### Run container ```sh @@ -1365,5 +1365,5 @@ docker run -d -p 5050:5050 frankescobar/allure-docker-service ``` ### Download specific tagged image registered (Example) ```sh -docker run -d -p 5050:5050 frankescobar/allure-docker-service:2.13.7 +docker run -d -p 5050:5050 frankescobar/allure-docker-service:2.13.8 ``` diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index b5f31a6..3a375ca 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -10,7 +10,7 @@ services: context: ../allure-docker-service dockerfile: docker-custom/Dockerfile.bionic-custom args: - ALLURE_RELEASE: "2.13.7" + ALLURE_RELEASE: "2.13.8" environment: DEV_MODE: 0 CHECK_RESULTS_EVERY_SECONDS: NONE diff --git a/docker-custom/Dockerfile.bionic-custom b/docker-custom/Dockerfile.bionic-custom index 509d279..9e4b680 100644 --- a/docker-custom/Dockerfile.bionic-custom +++ b/docker-custom/Dockerfile.bionic-custom @@ -1,9 +1,9 @@ ARG ARCH=amd64 ARG JDK=adoptopenjdk:11-jre-openj9-bionic ARG BUILD_DATE -ARG BUILD_VERSION=2.13.7-custom +ARG BUILD_VERSION=2.13.8-custom ARG BUILD_REF=na -ARG ALLURE_RELEASE=2.13.7 +ARG ALLURE_RELEASE=2.13.8 ARG ALLURE_REPO=https://dl.bintray.com/qameta/maven/io/qameta/allure/allure-commandline ARG UID=1000 ARG GID=1000 From 04d2a35ffeaaea51ead5868a988801c22d4bbca7 Mon Sep 17 00:00:00 2001 From: Frank Escobar Date: Wed, 13 Jan 2021 17:53:49 +0000 Subject: [PATCH 2/5] #146-Make viewer endpoints public when the security is enabled --- README.md | 16 ++++ allure-docker-api/app.py | 198 +++++++++++++++++++++++++++++---------- docker-compose-dev.yml | 5 +- 3 files changed, 169 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 3e2537d..b21db54 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Table of contents * [Refresh Access Token](#refresh-access-token) * [Logout](#logout) * [Roles](#roles) + * [Make Viewer endpoints public](#make-viewer-endpoints-public) * [Scripts](#scripts) * [Add Custom URL Prefix](#add-custom-url-prefix) * [Optimize Storage](#optimize-storage) @@ -1111,6 +1112,21 @@ Note: - `SECURITY_USER` & `SECURITY_VIEWER_USER` always need to be different. - Check [Allure API](#allure-api) to see what endpoints are exclusively for the `ADMIN` role. +##### Make Viewer endpoints public +`Available from Allure Docker Service version 2.13.8` +If you only want to protect the `Admin` endpoints and make public the viewer endpoints, then you can use the environment variable `MAKE_VIEWER_ENDPOINTS_PUBLIC` to make accessible the endpoints: + +Docker Compose example: +```sh + environment: + SECURITY_USER: "my_username" + SECURITY_PASS: "my_password" + SECURITY_ENABLED: 1 + MAKE_VIEWER_ENDPOINTS_PUBLIC: 1 +``` +Note: +- With `MAKE_VIEWER_ENDPOINTS_PUBLIC` enabled, your `viewer` user (if you have someone defined) won't have effect. + ##### Scripts - Bash script with security enabled: [allure-docker-api-usage/send_results_security.sh](allure-docker-api-usage/send_results_security.sh) ```sh diff --git a/allure-docker-api/app.py b/allure-docker-api/app.py index 2fd6fb6..4f086b8 100644 --- a/allure-docker-api/app.py +++ b/allure-docker-api/app.py @@ -81,6 +81,7 @@ def __str__(self): URL_PREFIX = '' OPTIMIZE_STORAGE = 0 ENABLE_SECURITY_LOGIN = False +MAKE_VIEWER_ENDPOINTS_PUBLIC = False SECURITY_USER = None SECURITY_PASS = None SECURITY_VIEWER_USER = None @@ -88,6 +89,53 @@ def __str__(self): USERS_INFO = {} ADMIN_ROLE_NAME = 'admin' VIEWER_ROLE_NAME = 'viewer' +PROTECTED_ENDPOINTS = [ + { + "method": "post", + "path": "/refresh", + "endpoint": "refresh_endpoint" + }, + { + "method": "delete", + "path": "/logout", + "endpoint": "logout_endpoint" + }, + { + "method": "delete", + "path": "/logout-refresh-token", + "endpoint": "logout_refresh_token_endpoint" + }, + { + "method": "post", + "path": "/send-results", + "endpoint": "send_results_endpoint" + }, + { + "method": "get", + "path": "/generate-report", + "endpoint": "generate_report_endpoint" + }, + { + "method": "get", + "path": "/clean-results", + "endpoint": "clean_results_endpoint" + }, + { + "method": "get", + "path": "/clean-history", + "endpoint": "clean_history_endpoint" + }, + { + "method": "post", + "path": "/projects", + "endpoint": "create_project_endpoint" + }, + { + "method": "delete", + "path": "/projects/{id}", + "endpoint": "delete_project_endpoint" + } +] GENERATE_REPORT_PROCESS = '{}/generateAllureReport.sh'.format(os.environ['ROOT']) KEEP_HISTORY_PROCESS = '{}/keepAllureHistory.sh'.format(os.environ['ROOT']) @@ -122,15 +170,23 @@ def __str__(self): if "API_RESPONSE_LESS_VERBOSE" in os.environ: try: - API_RESPONSE_LESS_VERBOSE = int(os.environ['API_RESPONSE_LESS_VERBOSE']) - LOGGER.info('Overriding API_RESPONSE_LESS_VERBOSE=%s', API_RESPONSE_LESS_VERBOSE) + API_RESPONSE_LESS_VERBOSE_TMP = int(os.environ['API_RESPONSE_LESS_VERBOSE']) + if API_RESPONSE_LESS_VERBOSE_TMP in (1, 0): + API_RESPONSE_LESS_VERBOSE = API_RESPONSE_LESS_VERBOSE_TMP + LOGGER.info('Overriding API_RESPONSE_LESS_VERBOSE=%s', API_RESPONSE_LESS_VERBOSE) + else: + LOGGER.error('Wrong env var value. Setting API_RESPONSE_LESS_VERBOSE=0 by default') except Exception as ex: LOGGER.error('Wrong env var value. Setting API_RESPONSE_LESS_VERBOSE=0 by default') if "DEV_MODE" in os.environ: try: - DEV_MODE = int(os.environ['DEV_MODE']) - LOGGER.info('Overriding DEV_MODE=%s', DEV_MODE) + DEV_MODE_TMP = int(os.environ['DEV_MODE']) + if DEV_MODE_TMP in (1, 0): + DEV_MODE = DEV_MODE_TMP + LOGGER.info('Overriding DEV_MODE=%s', DEV_MODE) + else: + LOGGER.error('Wrong env var value. Setting DEV_MODE=0 by default') except Exception as ex: LOGGER.error('Wrong env var value. Setting DEV_MODE=0 by default') @@ -147,7 +203,7 @@ def __str__(self): if "URL_PREFIX" in os.environ: PREFIX = str(os.environ['URL_PREFIX']) if DEV_MODE == 1: - LOGGER.info('URL_PREFIX is not supported when DEV_MODE is enabled') + LOGGER.warning('URL_PREFIX is not supported when DEV_MODE is enabled') else: if PREFIX and PREFIX.strip(): if PREFIX.startswith('/') is False: @@ -160,11 +216,24 @@ def __str__(self): if "OPTIMIZE_STORAGE" in os.environ: try: - OPTIMIZE_STORAGE = int(os.environ['OPTIMIZE_STORAGE']) - LOGGER.info('Overriding OPTIMIZE_STORAGE=%s', OPTIMIZE_STORAGE) + OPTIMIZE_STORAGE_TMP = int(os.environ['OPTIMIZE_STORAGE']) + if OPTIMIZE_STORAGE_TMP in (1, 0): + OPTIMIZE_STORAGE = OPTIMIZE_STORAGE_TMP + LOGGER.info('Overriding OPTIMIZE_STORAGE=%s', OPTIMIZE_STORAGE) + else: + LOGGER.error('Wrong env var value. Setting OPTIMIZE_STORAGE=0 by default') except Exception as ex: LOGGER.error('Wrong env var value. Setting OPTIMIZE_STORAGE=0 by default') +if "MAKE_VIEWER_ENDPOINTS_PUBLIC" in os.environ: + try: + VIEWER_ENDPOINTS_PUBLIC_TMP = int(os.environ['MAKE_VIEWER_ENDPOINTS_PUBLIC']) + if VIEWER_ENDPOINTS_PUBLIC_TMP == 1: + MAKE_VIEWER_ENDPOINTS_PUBLIC = True + LOGGER.info('Overriding MAKE_VIEWER_ENDPOINTS_PUBLIC=%s', VIEWER_ENDPOINTS_PUBLIC_TMP) + except Exception as ex: + LOGGER.error('Wrong env var value. Setting VIEWER_ENDPOINTS_PUBLIC=0 by default') + if "SECURITY_USER" in os.environ: SECURITY_USER_TMP = os.environ['SECURITY_USER'] if SECURITY_USER_TMP and SECURITY_USER_TMP.strip(): @@ -177,17 +246,18 @@ def __str__(self): SECURITY_PASS = SECURITY_PASS_TMP LOGGER.info('Setting SECURITY_PASS') -if "SECURITY_VIEWER_USER" in os.environ: - SECURITY_VIEWER_USER_TMP = os.environ['SECURITY_VIEWER_USER'] - if SECURITY_VIEWER_USER_TMP and SECURITY_VIEWER_USER_TMP.strip(): - SECURITY_VIEWER_USER = SECURITY_VIEWER_USER_TMP.lower() - LOGGER.info('Setting SECURITY_VIEWER_USER') +if MAKE_VIEWER_ENDPOINTS_PUBLIC is False: + if "SECURITY_VIEWER_USER" in os.environ: + SECURITY_VIEWER_USER_TMP = os.environ['SECURITY_VIEWER_USER'] + if SECURITY_VIEWER_USER_TMP and SECURITY_VIEWER_USER_TMP.strip(): + SECURITY_VIEWER_USER = SECURITY_VIEWER_USER_TMP.lower() + LOGGER.info('Setting SECURITY_VIEWER_USER') -if "SECURITY_VIEWER_PASS" in os.environ: - SECURITY_VIEWER_PASS_TMP = os.environ['SECURITY_VIEWER_PASS'] - if SECURITY_VIEWER_PASS_TMP and SECURITY_VIEWER_PASS_TMP.strip(): - SECURITY_VIEWER_PASS = SECURITY_VIEWER_PASS_TMP - LOGGER.info('Setting SECURITY_VIEWER_PASS') + if "SECURITY_VIEWER_PASS" in os.environ: + SECURITY_VIEWER_PASS_TMP = os.environ['SECURITY_VIEWER_PASS'] + if SECURITY_VIEWER_PASS_TMP and SECURITY_VIEWER_PASS_TMP.strip(): + SECURITY_VIEWER_PASS = SECURITY_VIEWER_PASS_TMP + LOGGER.info('Setting SECURITY_VIEWER_PASS') if "SECURITY_ENABLED" in os.environ: try: @@ -201,10 +271,11 @@ def __str__(self): 'pass': SECURITY_PASS, 'roles': [ADMIN_ROLE_NAME] } - USERS_INFO[SECURITY_VIEWER_USER] = { - 'pass': SECURITY_VIEWER_PASS, - 'roles': [VIEWER_ROLE_NAME] - } + if SECURITY_VIEWER_USER is not None and SECURITY_VIEWER_PASS is not None: + USERS_INFO[SECURITY_VIEWER_USER] = { + 'pass': SECURITY_VIEWER_PASS, + 'roles': [VIEWER_ROLE_NAME] + } else: LOGGER.info('Setting SECURITY_ENABLED=0 by default') else: @@ -292,6 +363,24 @@ def get_security_specs(): security_specs[file] = eval(get_file_as_string(file_path)) #pylint: disable=eval-used return security_specs +def is_endpoint_protected(endpoint): + if MAKE_VIEWER_ENDPOINTS_PUBLIC is False: + return True + + for info in PROTECTED_ENDPOINTS: + if endpoint == info['endpoint']: + return True + return False + +def is_endpoint_swagger_protected(method, path): + if MAKE_VIEWER_ENDPOINTS_PUBLIC is False: + return True + + for info in PROTECTED_ENDPOINTS: + if info['method'] == method and path == info['path']: + return True + return False + def generate_security_swagger_spec(): try: security_specs = get_security_specs() @@ -309,20 +398,20 @@ def generate_security_swagger_spec(): security_401_response = security_specs['security_unauthorized_response.json'] security_403_response = security_specs['security_forbidden_response.json'] security_crsf = security_specs['security_csrf.json'] - for path in data['paths']: + for path in data['paths']: #pylint: disable=too-many-nested-blocks for method in data['paths'][path]: - if set(ensure_tags) & set(data['paths'][path][method]['tags']): - data['paths'][path][method]['security'] = security_type - data['paths'][path][method]['responses']['401'] = security_401_response - data['paths'][path][method]['responses']['403'] = security_403_response - if method in ['post', 'put', 'patch', 'delete']: - if 'parameters' in data['paths'][path][method]: - params = data['paths'][path][method]['parameters'] - params.append(security_crsf) - data['paths'][path][method]['parameters'] = params - else: - data['paths'][path][method]['parameters'] = [security_crsf] - + if is_endpoint_swagger_protected(method, path): + if set(ensure_tags) & set(data['paths'][path][method]['tags']): + data['paths'][path][method]['security'] = security_type + data['paths'][path][method]['responses']['401'] = security_401_response + data['paths'][path][method]['responses']['403'] = security_403_response + if method in ['post', 'put', 'patch', 'delete']: + if 'parameters' in data['paths'][path][method]: + params = data['paths'][path][method]['parameters'] + params.append(security_crsf) + data['paths'][path][method]['parameters'] = params + else: + data['paths'][path][method]['parameters'] = [security_crsf] with open("{}/swagger/swagger_security.json".format(STATIC_CONTENT), 'w') as outfile: json.dump(data, outfile) except Exception as ex: @@ -406,7 +495,8 @@ def jwt_required(fn): #pylint: disable=invalid-name, function-redefined @wraps(fn) def wrapper(*args, **kwargs): if ENABLE_SECURITY_LOGIN: - verify_jwt_in_request() + if is_endpoint_protected(request.endpoint): + verify_jwt_in_request() return fn(*args, **kwargs) return wrapper @@ -414,7 +504,8 @@ def jwt_refresh_token_required(fn): #pylint: disable=invalid-name, function-rede @wraps(fn) def wrapper(*args, **kwargs): if ENABLE_SECURITY_LOGIN: - verify_jwt_refresh_token_in_request() + if is_endpoint_protected(request.endpoint): + verify_jwt_refresh_token_in_request() return fn(*args, **kwargs) return wrapper @@ -665,12 +756,14 @@ def config_endpoint(): check_results_every_seconds = os.getenv('CHECK_RESULTS_EVERY_SECONDS', '1') keep_history = os.getenv('KEEP_HISTORY', '0') keep_history_latest = os.getenv('KEEP_HISTORY_LATEST', '20') - tls = os.getenv('TLS', '0') + tls = int(app.config['JWT_COOKIE_SECURE']) security_enabled = int(ENABLE_SECURITY_LOGIN) + make_viewer_endpoints_public = int(MAKE_VIEWER_ENDPOINTS_PUBLIC) body = { 'data': { 'version': version, + 'dev_mode': DEV_MODE, 'check_results_every_seconds': check_results_every_seconds, 'keep_history': keep_history, 'keep_history_latest': keep_history_latest, @@ -678,7 +771,8 @@ def config_endpoint(): 'security_enabled': security_enabled, 'url_prefix': URL_PREFIX, 'api_response_less_verbose': API_RESPONSE_LESS_VERBOSE, - 'optimize_storage': OPTIMIZE_STORAGE + 'optimize_storage': OPTIMIZE_STORAGE, + "make_viewer_endpoints_public": make_viewer_endpoints_public }, 'meta_data': { 'message' : "Config successfully obtained" @@ -756,7 +850,7 @@ def latest_report_endpoint(): @jwt_required def send_results_endpoint(): #pylint: disable=too-many-branches try: - if check_access(ADMIN_ROLE_NAME) is False: + if check_admin_access(current_user) is False: return jsonify({ 'meta_data': { 'message': 'Access Forbidden' } }), 403 content_type = str(request.content_type) @@ -852,7 +946,7 @@ def send_results_endpoint(): #pylint: disable=too-many-branches @jwt_required def generate_report_endpoint(): try: - if check_access(ADMIN_ROLE_NAME) is False: + if check_admin_access(current_user) is False: return jsonify({ 'meta_data': { 'message': 'Access Forbidden' } }), 403 project_id = resolve_project(request.args.get('project_id')) @@ -945,7 +1039,7 @@ def generate_report_endpoint(): @jwt_required def clean_history_endpoint(): try: - if check_access(ADMIN_ROLE_NAME) is False: + if check_admin_access(current_user) is False: return jsonify({ 'meta_data': { 'message': 'Access Forbidden' } }), 403 project_id = resolve_project(request.args.get('project_id')) @@ -986,7 +1080,7 @@ def clean_history_endpoint(): @jwt_required def clean_results_endpoint(): try: - if check_access(ADMIN_ROLE_NAME) is False: + if check_admin_access(current_user) is False: return jsonify({ 'meta_data': { 'message': 'Access Forbidden' } }), 403 project_id = resolve_project(request.args.get('project_id')) @@ -1176,7 +1270,7 @@ def report_export_endpoint(): @jwt_required def create_project_endpoint(): try: - if check_access(ADMIN_ROLE_NAME) is False: + if check_admin_access(current_user) is False: return jsonify({ 'meta_data': { 'message': 'Access Forbidden' } }), 403 if not request.is_json: @@ -1209,7 +1303,7 @@ def create_project_endpoint(): @jwt_required def delete_project_endpoint(project_id): try: - if check_access(ADMIN_ROLE_NAME) is False: + if check_admin_access(current_user) is False: return jsonify({ 'meta_data': { 'message': 'Access Forbidden' } }), 403 if project_id == 'default': @@ -1536,14 +1630,20 @@ def resolve_project(project_id_param): project_id = project_id_param return project_id -def check_access(role): +def check_admin_access(user): if ENABLE_SECURITY_LOGIN is False: return True - granted = False - if role in current_user.roles: - granted = True - return granted + return check_access(ADMIN_ROLE_NAME, user) + +def check_access(role, user): + if user.roles is None: + return False + + if role in user.roles: + return True + + return False def check_process(process_file, project_id): tmp = os.popen('ps -Af | grep -w {}'.format(project_id)).read() diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 3a375ca..d1a382c 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -21,8 +21,11 @@ services: SECURITY_PASS: "my_password" SECURITY_VIEWER_USER: "view_user" SECURITY_VIEWER_PASS: "view_pass" - SECURITY_ENABLED: 0 + SECURITY_ENABLED: 1 + MAKE_VIEWER_ENDPOINTS_PUBLIC: 0 OPTIMIZE_STORAGE: 0 + API_RESPONSE_LESS_VERBOSE: 0 + TLS: 0 ports: - "${ALLURE_DOCKER_SERVICE_API_PORT}:5050" volumes: From c4928ae66245c05dcb7d131b940d31c53df06fe9 Mon Sep 17 00:00:00 2001 From: Frank Escobar Date: Sat, 16 Jan 2021 11:50:34 +0000 Subject: [PATCH 3/5] Moving from Travis to GH Actions --- .github/workflows/docker-publish.yml | 173 ++++++++++++++++++++++++++ .travis.yml => deprecated/.travis.yml | 0 {docker => deprecated}/docker.sh | 0 3 files changed, 173 insertions(+) create mode 100644 .github/workflows/docker-publish.yml rename .travis.yml => deprecated/.travis.yml (100%) rename {docker => deprecated}/docker.sh (100%) mode change 100755 => 100644 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..1507d89 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,173 @@ +name: Allure Docker Service Workflow + +on: + push: + branches: + - "*" + + tags: + - v* + + pull_request: + +env: + DOCKER_IMAGE: frankescobar/allure-docker-service + ALLURE_RELEASE: 2.13.8 + QEMU_VERSION: v4.0.0 + DOCKER_CLI_EXPERIMENTAL: enabled + +jobs: + build_release: + runs-on: ubuntu-latest + strategy: + matrix: + ARCH: [amd64, arm32v7, arm64v8] + include: + - ARCH: amd64 + DOCKER_FILE: Dockerfile.bionic + JDK: adoptopenjdk:11-jre-openj9-bionic + QEMU_ARCH: x86_64 + + - ARCH: arm32v7 + DOCKER_FILE: Dockerfile.bionic + JDK: adoptopenjdk:11-jdk-hotspot-bionic + QEMU_ARCH: arm + + - ARCH: arm64v8 + DOCKER_FILE: Dockerfile.bionic + JDK: adoptopenjdk:11-jre-hotspot-bionic + QEMU_ARCH: aarch64 + + if: github.event_name == 'push' + outputs: + build_version: ${{ steps.prepare.outputs.build_version }} + steps: + - name: Pulling code + uses: actions/checkout@v2 + + - name: Preparing + id: prepare + run: | + echo "DOCKER BUILD: Build Docker image." + echo "DOCKER BUILD: arch - ${{matrix.ARCH}}." + echo "DOCKER BUILD: jdk -> ${{matrix.JDK}}." + echo "DOCKER BUILD: build version -> ${VERSION}." + echo "DOCKER BUILD: allure version -> ${ALLURE_RELEASE}." + echo "DOCKER BUILD: qemu arch - ${{matrix.QEMU_ARCH}}." + echo "DOCKER BUILD: docker file - ${{matrix.DOCKER_FILE}}." + + VERSION=na + TAGS="--tag ${DOCKER_IMAGE}:build" + + if [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION=${GITHUB_REF#refs/tags/v} + if [[ $GITHUB_REF == *"beta"* ]]; then + TAGS="--tag ${DOCKER_IMAGE}:${VERSION} --tag ${DOCKER_IMAGE}:${VERSION}-${{matrix.ARCH}} --tag ${DOCKER_IMAGE}:beta --tag ${DOCKER_IMAGE}:build" + else + TAGS="--tag ${DOCKER_IMAGE}:${VERSION} --tag ${DOCKER_IMAGE}:${VERSION}-${{matrix.ARCH}} --tag ${DOCKER_IMAGE}:latest --tag ${DOCKER_IMAGE}:build" + fi + fi + + echo ::set-output name=docker_image::${DOCKER_IMAGE} + echo ::set-output name=build_version::${VERSION} + echo ::set-output name=docker_args::--build-arg ARCH=${{matrix.ARCH}} \ + --build-arg JDK=${{matrix.JDK}} \ + --build-arg QEMU_ARCH=${{matrix.QEMU_ARCH}} \ + --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ + --build-arg BUILD_VERSION=${VERSION} \ + --build-arg BUILD_REF=${GITHUB_SHA::8} \ + --build-arg ALLURE_RELEASE=${ALLURE_RELEASE} \ + ${TAGS} --file docker/Dockerfile.bionic . + + - name: Setting up QEMU + run: | + # Prepare qemu to build non amd64 / x86_64 images + docker run --rm --privileged multiarch/qemu-user-static:register --reset + mkdir tmp + pushd tmp && + curl -L -o qemu-x86_64-static.tar.gz https://github.com/multiarch/qemu-user-static/releases/download/$QEMU_VERSION/qemu-x86_64-static.tar.gz && tar xzf qemu-x86_64-static.tar.gz && + curl -L -o qemu-arm-static.tar.gz https://github.com/multiarch/qemu-user-static/releases/download/$QEMU_VERSION/qemu-arm-static.tar.gz && tar xzf qemu-arm-static.tar.gz && + curl -L -o qemu-aarch64-static.tar.gz https://github.com/multiarch/qemu-user-static/releases/download/$QEMU_VERSION/qemu-aarch64-static.tar.gz && tar xzf qemu-aarch64-static.tar.gz && + popd + + - name: Docker Building + run: | + docker build --no-cache ${{ steps.prepare.outputs.docker_args }} + + - name: Docker Testing + run: | + echo "DOCKER TEST: Test Docker image." + echo "DOCKER TEST: testing image -> ${DOCKER_IMAGE}:build" + + docker run -d --rm --name=testing ${DOCKER_IMAGE}:build + if [ $? -ne 0 ]; then + echo "DOCKER TEST: FAILED - Docker container testing failed to start." + exit 1 + else + echo "DOCKER TEST: PASSED - Docker container testing succeeded to start." + fi + + - name: DockerHub Login + if: success() && startsWith(github.ref, 'refs/tags/v') + env: + DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }} + DOCKER_HUB_PASS: ${{ secrets.DOCKER_HUB_PASS }} + run: | + echo "${DOCKER_HUB_PASS}" | docker login -u "${DOCKER_HUB_USER}" --password-stdin + + - name: Docker Publishing + if: success() && startsWith(github.ref, 'refs/tags/v') + run: | + echo "DOCKER PUSH: pushing - ${DOCKER_IMAGE}:${{ steps.prepare.outputs.build_version }}-${{matrix.ARCH}}." + docker push ${DOCKER_IMAGE}:${{ steps.prepare.outputs.build_version }}-${{matrix.ARCH}} + + - name: Docker Logout + if: success() && startsWith(github.ref, 'refs/tags/v') + run: | + docker logout + + manifest_release: + runs-on: ubuntu-latest + needs: build_release + steps: + - name: DockerHub Login + if: success() && startsWith(github.ref, 'refs/tags/v') + env: + DOCKER_HUB_USER: ${{ secrets.DOCKER_HUB_USER }} + DOCKER_HUB_PASS: ${{ secrets.DOCKER_HUB_PASS }} + run: | + echo "${DOCKER_HUB_PASS}" | docker login -u "${DOCKER_HUB_USER}" --password-stdin + + - name: Docker Publishing Manifest + if: success() && startsWith(github.ref, 'refs/tags/v') + run: | + BUILD_VERSION=${{ needs.build_release.outputs.build_version }} + docker manifest create ${DOCKER_IMAGE}:${BUILD_VERSION} \ + ${DOCKER_IMAGE}:${BUILD_VERSION}-amd64 \ + ${DOCKER_IMAGE}:${BUILD_VERSION}-arm32v7 \ + ${DOCKER_IMAGE}:${BUILD_VERSION}-arm64v8 + + docker manifest annotate ${DOCKER_IMAGE}:${BUILD_VERSION} ${DOCKER_IMAGE}:${BUILD_VERSION}-arm32v7 --os=linux --arch=arm --variant=v7 + docker manifest annotate ${DOCKER_IMAGE}:${BUILD_VERSION} ${DOCKER_IMAGE}:${BUILD_VERSION}-arm64v8 --os=linux --arch=arm64 --variant=v8 + + docker manifest push ${DOCKER_IMAGE}:${BUILD_VERSION} + + TAG=beta + if [[ ${BUILD_VERSION} != *"beta"* ]]; then + TAG=latest + fi + + docker manifest create ${DOCKER_IMAGE}:${TAG} \ + ${DOCKER_IMAGE}:${BUILD_VERSION}-amd64 \ + ${DOCKER_IMAGE}:${BUILD_VERSION}-arm32v7 \ + ${DOCKER_IMAGE}:${BUILD_VERSION}-arm64v8 + + docker manifest annotate ${DOCKER_IMAGE}:${TAG} ${DOCKER_IMAGE}:${BUILD_VERSION}-arm32v7 --os=linux --arch=arm --variant=v7 + docker manifest annotate ${DOCKER_IMAGE}:${TAG} ${DOCKER_IMAGE}:${BUILD_VERSION}-arm64v8 --os=linux --arch=arm64 --variant=v8 + + docker manifest push ${DOCKER_IMAGE}:${TAG} + + - name: Docker Logout + if: success() && startsWith(github.ref, 'refs/tags/v') + run: | + docker logout diff --git a/.travis.yml b/deprecated/.travis.yml similarity index 100% rename from .travis.yml rename to deprecated/.travis.yml diff --git a/docker/docker.sh b/deprecated/docker.sh old mode 100755 new mode 100644 similarity index 100% rename from docker/docker.sh rename to deprecated/docker.sh From bd501414b592b2aa5ae4d2be6dbdaa9fc5dd6d55 Mon Sep 17 00:00:00 2001 From: Frank Escobar Date: Mon, 18 Jan 2021 13:06:50 +0000 Subject: [PATCH 4/5] #136-clean_results doesnt work with a large amount of results --- allure-docker-scripts/cleanAllureResults.sh | 2 +- tests/emulateResultsFiles.sh | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/emulateResultsFiles.sh diff --git a/allure-docker-scripts/cleanAllureResults.sh b/allure-docker-scripts/cleanAllureResults.sh index cb76b3b..08c0168 100644 --- a/allure-docker-scripts/cleanAllureResults.sh +++ b/allure-docker-scripts/cleanAllureResults.sh @@ -5,7 +5,7 @@ PROJECT_ID=$1 echo "Cleaning results for PROJECT_ID: $PROJECT_ID" PROJECT_RESULTS_DIRECTORY=$STATIC_CONTENT_PROJECTS/$PROJECT_ID/results if [ "$(ls -A $PROJECT_RESULTS_DIRECTORY | wc -l)" != "0" ]; then - ls -d $PROJECT_RESULTS_DIRECTORY/* | grep -v history | xargs rm 2> /dev/null + find $PROJECT_RESULTS_DIRECTORY/ -maxdepth 1 -type f -print0 | xargs -0 rm 2> /dev/null fi echo "Results cleaned for PROJECT_ID: $PROJECT_ID" diff --git a/tests/emulateResultsFiles.sh b/tests/emulateResultsFiles.sh new file mode 100644 index 0000000..ecd401c --- /dev/null +++ b/tests/emulateResultsFiles.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +MAX=40000; +COUNTER=1; +PROJECT_RESULTS_DIRECTORY=$STATIC_CONTENT_PROJECTS/default/results + +while ((COUNTER<=MAX)); do + touch $PROJECT_RESULTS_DIRECTORY/$((COUNTER)); + COUNTER=$((COUNTER+1)); +done +mkdir -p $PROJECT_RESULTS_DIRECTORY/history From a1a265300f40e803e948d835517af4805521d02c Mon Sep 17 00:00:00 2001 From: Frank Escobar Date: Mon, 18 Jan 2021 16:04:31 +0000 Subject: [PATCH 5/5] Adding info to README about "Customize Executors Configuration" --- README.md | 10 ++++++++++ resources/executor06.png | Bin 0 -> 4553 bytes resources/executor07.png | Bin 0 -> 4242 bytes 3 files changed, 10 insertions(+) create mode 100644 resources/executor06.png create mode 100644 resources/executor07.png diff --git a/README.md b/README.md index b21db54..e95162a 100644 --- a/README.md +++ b/README.md @@ -688,6 +688,16 @@ If the type is not recognized it will take the default icon. You can use differe [![](resources/executor05.png)](resources/executor05.png) +- github + +[![](resources/executor06.png)](resources/executor06.png) + + +- gitlab + +[![](resources/executor07.png)](resources/executor07.png) + + The icons are based on the native Allure2 Framework: - https://github.com/allure-framework/allure2/tree/master/allure-generator/src/main/javascript/blocks/executor-icon diff --git a/resources/executor06.png b/resources/executor06.png new file mode 100644 index 0000000000000000000000000000000000000000..652e5c7bed0d30317fe7b2cf2b235fe87e8f0fa9 GIT binary patch literal 4553 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 Hw#a`0&GgNv literal 0 HcmV?d00001 diff --git a/resources/executor07.png b/resources/executor07.png new file mode 100644 index 0000000000000000000000000000000000000000..1a498294a5b60632396e3b69532b16144bcd8433 GIT binary patch literal 4242 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