From 3bc520f59361021ce5066a99f72a7bbb7fdebc0e Mon Sep 17 00:00:00 2001 From: Michiel Korpel Date: Wed, 18 Dec 2024 10:15:54 +0100 Subject: [PATCH] Add scripts, Dockerfile, etc. --- .dockerignore | 1 + .github/workflows/build-and-publish-image.yml | 67 ++++++ .gitignore | 3 + Dockerfile | 32 +++ README.md | 33 +++ scripts/parse-results.py | 226 ++++++++++++++++++ scripts/requirements.txt | 2 + scripts/startup.sh | 47 ++++ 8 files changed, 411 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/build-and-publish-image.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100755 scripts/parse-results.py create mode 100644 scripts/requirements.txt create mode 100755 scripts/startup.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ea1472e --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +output/ diff --git a/.github/workflows/build-and-publish-image.yml b/.github/workflows/build-and-publish-image.yml new file mode 100644 index 0000000..61a86bd --- /dev/null +++ b/.github/workflows/build-and-publish-image.yml @@ -0,0 +1,67 @@ +--- +name: build +env: + image: pdok/ets-ogcapi-tiles10-docker +on: + push: + tags: + - '*' +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Docker meta + id: docker_meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.image }} + tags: | + type=semver,pattern={{major}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{version}} + - name: Login to PDOK Docker Hub + if: startsWith(env.image, 'pdok/') + uses: docker/login-action@v1 + with: + username: koalapdok + password: ${{ secrets.DOCKERHUB_PUSH }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new + - # Temp fix to cleanup cache + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + - name: Build result notification + if: success() || failure() + uses: 8398a7/action-slack@v3 + with: + fields: all + status: custom + custom_payload: | + { + attachments: [{ + color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', + text: `${process.env.AS_WORKFLOW} ${{ job.status }} for ${process.env.AS_REPO}!\n${process.env.AS_JOB} job on ${process.env.AS_REF} (commit: ${process.env.AS_COMMIT}, version: ${{ steps.docker_meta.outputs.version }}) by ${process.env.AS_AUTHOR} took ${process.env.AS_TOOK}`, + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea1ef32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +output/ +test-run-props.xml +example/kubeconfig.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..31886ea --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM docker.io/maven:3-eclipse-temurin-8 + +# ARG REPO=https://github.com/opengeospatial/ets-ogcapi-tiles10.git +# ARG REPO_REF="tags/1.1" +# temporary, until PRs are approved/merged +ARG REPO=https://github.com/kad-korpem/ets-ogcapi-tiles10.git +ARG REPO_REF="allow-204-within-limits" + +WORKDIR /src +RUN git clone ${REPO} . && git checkout ${REPO_REF} + +# temporary, until PRs are approved/merged +RUN mvn spring-javaformat:apply + +RUN mvn clean install +RUN mv /src/target/ets-ogcapi-tiles10-*-aio.jar /src/target/ets-ogcapi-tiles10-aio.jar + +FROM docker.io/eclipse-temurin:21-jre +RUN apt update && apt install -y python3 \ + python3-pip + +WORKDIR /src +COPY scripts /src + +RUN python3 -m pip config set global.break-system-packages true +RUN python3 -m pip install -r requirements.txt +LABEL AUTHOR="pdok@kadaster.nl" +# set correct timezone +ENV TZ=Europe/Amsterdam + +COPY --from=0 /src/target/ets-ogcapi-tiles10-aio.jar /opt/ets-ogcapi-tiles10-aio.jar +ENTRYPOINT ["bash", "/src/startup.sh"] diff --git a/README.md b/README.md index 6e34703..348eabb 100644 --- a/README.md +++ b/README.md @@ -1 +1,34 @@ # ets-ogcapi-tiles10-docker + +[![Docker Pulls](https://badgen.net/docker/pulls/pdok/ets-ogcapi-tiles10-docker?icon=docker&label=pulls)](https://hub.docker.com/r/pdok/ets-ogcapi-tiles10-docker/) + +PDOK Docker image for [OGC API - Tiles Compliance Test Suite](https://github.com/opengeospatial/ets-ogcapi-tiles10) for command-line use, with additional features: + +- pass service url as command-line argument +- when passing `-exitOnFail` flag, return code `0` if test suite passes all tests, otherwise `1` (instead of always returning `0`) + +## Usage examples + +```bash +docker run -t -v "$(pwd):/mnt" pdok/ets-ogcapi-tiles10-docker:latest https://api.pdok.nl/lv/bag/ogc/v1/ --generateHtmlReport true --outputDir /mnt/output --exitOnFail --prettyPrint +``` + +```bash +URL=https://api.pdok.nl/lv/bag/ogc/v1/ +cat > ./test-run-props.xml < + + + Test run arguments + ${URL} + http://www.opengis.net/def/tilematrixset/OGC/1.0/WebMercatorQuad + ${URL}/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}?f=mvt + 17 + 67500 + 67510 + 43200 + 43210 + +EOF +docker run -v "$(pwd):/mnt" pdok/ets-ogcapi-tiles10-docker:latest /mnt/test-run-props.xml --generateHtmlReport true --outputDir /mnt/output +``` diff --git a/scripts/parse-results.py b/scripts/parse-results.py new file mode 100755 index 0000000..3b46e5a --- /dev/null +++ b/scripts/parse-results.py @@ -0,0 +1,226 @@ +#!/bin/env python3 +import argparse +from pathlib import Path +from rich.console import Console +from rich.table import Table +from junitparser import JUnitXml, Failure, Error, Skipped + +err_console = Console(stderr=True) +console = Console() + +failed_table = Table(show_lines=True) +errored_table = Table(show_lines=True) +skipped_table = Table(show_lines=True) + + +def get_api_docs_url(test_xml_name, name): + return f'https://cite.opengeospatial.org/te2/about/ogcapi-tiles-1.0/1.0/site/apidocs/index.html?{test_xml_name.replace(".","/")}.html ({name})' + + +def add_table(table, tuples, title): + table.add_column( + "Case Name", justify="right", style="cyan", no_wrap=False, overflow="fold" + ) + table.add_column( + "Error", justify="right", style="red", no_wrap=False, overflow="fold" + ) + table.add_column( + "File", justify="right", style="magenta", no_wrap=False, overflow="fold" + ) + table.add_column( + "Url", justify="right", style="orange1", no_wrap=False, overflow="fold" + ) + for case in tuples: + table.add_row(case[1], case[2], str(case[0]), case[3]) + + table.title = title + console.print(table) + + +def main(result_dir, service_url, pretty_print, exit_on_fail): + """ + Parse junit result. + """ + + failed_cases = [] + failed_tuples = [] + + skipped_cases = [] + skipped_tuples = [] + + errored_cases = [] + errored_tuples = [] + + dir_path = Path(args.result_dir) + for junit_test in dir_path.glob("**/**/TEST-org.opengis.cite.*.xml"): + test_xml = JUnitXml.fromfile(str(junit_test)) + + failed = [ + case + for case in test_xml + if any(isinstance(item, Failure) for item in case.result) + ] + failed_message = [result.message for fail in failed for result in fail.result] + failed_tuples += [ + ( + junit_test.name, + test_xml.name, + result.message, + get_api_docs_url(test_xml.name, fail.name), + ) + for fail in failed + for result in fail.result + ] + if failed: + fail_name = next(iter([fail.name for fail in failed]), "") + failed_cases += ( + [f"## {test_xml.name}"] + + [""] + + failed_message + + [""] + + [get_api_docs_url(test_xml.name, fail_name)] + + [""] + ) + + skipped = [ + case + for case in test_xml + if any(isinstance(item, Skipped) for item in case.result) + ] + errored_or_skipped = [ + case + for case in test_xml + if any(isinstance(item, Error) for item in case.result) + ] + if errored_or_skipped: + for case in errored_or_skipped: + # TestNG SkipExceptions are wrongfully marked as errored when using the JUnit reporter. + # Turn these errored tests into skipped tests. + if case.result[0].type == 'org.testng.SkipException': + skipped += [case] + else: + # Not a SkipException so treat as errored + errored = [case] + errored_message = [ + result.message for error in errored for result in error.result + ] + errored_tuples += [ + ( + junit_test.name, + test_xml.name, + result.message, + get_api_docs_url(test_xml.name, error.name), + ) + for error in errored + for result in error.result + ] + if errored: + error_name = next(iter([error.name for error in errored]), "") + errored_cases += ( + [f"## {test_xml.name}"] + + errored_message + + [""] + + [get_api_docs_url(test_xml.name, error_name)] + + [""] + ) + + # Handle skipped + skipped_message = [result.message for skip in skipped for result in skip.result] + skipped_tuples += [ + ( + junit_test.name, + test_xml.name, + result.message, + get_api_docs_url(test_xml.name, skip.name), + ) + for skip in skipped + for result in skip.result + ] + if skipped: + skip_name = next(iter([skip.name for skip in skipped]), "") + skipped_cases += ( + [f"## {test_xml.name}"] + + skipped_message + + [""] + + [get_api_docs_url(test_xml.name, skip_name)] + + [""] + ) + + if pretty_print: + console.print( + "ogcapi-tiles-1.0-1.1 Test Suite Run", + style="bold italic underline", + justify="center", + ) + console.print("\n") + console.print(f"Output test run saved in: '{result_dir}'", justify="center") + if service_url: + console.print(f"Test instance: '{service_url}'", justify="center") + console.print("\n") + + if failed_tuples: + add_table( + failed_table, failed_tuples, f"FAILED TEST CASES ({len(failed_tuples)})" + ) + + if errored_tuples: + add_table( + errored_table, + errored_tuples, + f"ERRORED TEST CASES ({len(errored_tuples)})", + ) + + if skipped_tuples: + add_table( + skipped_table, + skipped_tuples, + f"SKIPPED TEST CASES ({len(skipped_tuples)})", + ) + + else: + console.print("# ogcapi-tiles-1.0-1.1 Test Suite Run\n") + console.print(f"- Output test run saved in: '{result_dir}'") + if service_url: + console.print(f"- Test instance: '{service_url}'") + console.print("\n") + + if failed_cases: + console.print("# FAILED TEST CASES\n", style="red") + console.print("\n".join(failed_cases), style="red") + + if errored_cases: + console.print("# ERRORED TEST CASES\n", style="orange1") + console.print("\n".join(errored_cases), style="orange1") + + if skipped_cases: + console.print("# SKIPPED TEST CASES\n", style="yellow") + console.print("\n".join(skipped_cases), style="yellow") + + if failed_cases and exit_on_fail: + exit(1) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Parse OAF ETS results") + parser.add_argument( + "result_dir", type=str, help="Directory with the result to parse" + ) + parser.add_argument( + "--service-url", help="Optional service url to print to console" + ) + parser.add_argument( + "--pretty-print", action="store_true", help="Print with a better formatting" + ) + parser.add_argument( + "--exit-on-fail", + action="store_true", + help="Force failing with exit code 1 when failed tests cases in result", + ) + args = parser.parse_args() + + dir_path = Path(args.result_dir) + if not dir_path.exists(): + err_console.print(f"test dir '{args.result_dir}' should exist") + exit(1) + + main(args.result_dir, args.service_url, args.pretty_print, args.exit_on_fail) diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..0512f48 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,2 @@ +junitparser==2.7.0 +rich==12 diff --git a/scripts/startup.sh b/scripts/startup.sh new file mode 100755 index 0000000..6e1728a --- /dev/null +++ b/scripts/startup.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +run_props="$1" +service_url="" + +if grep -E "https?://" <<< "$run_props" > /dev/null;then + service_url="$run_props" + cat > /tmp/test-run-props.xml < + + + Test run arguments + ${run_props} + http://www.opengis.net/def/tilematrixset/OGC/1.0/WebMercatorQuad + ${run_props}/tiles/WebMercatorQuad/{tileMatrix}/{tileRow}/{tileCol}?f=mvt + 17 + 67500 + 67510 + 43200 + 43210 + +EOF + nr_args="$#" + set -- "/tmp/test-run-props.xml" "${@:2:$nr_args}" # replace first argument with path to test-run-props.xml +fi + +EXIT_ON_FAIL="" +if [[ $* == *--exitOnFail* ]];then + EXIT_ON_FAIL="--exit-on-fail" +fi + +PRETTY_PRINT="" +if [[ $* == *--prettyPrint* ]];then + PRETTY_PRINT="--pretty-print" +fi + +if [[ $* == *--verbose* ]];then + exec 5>&1 # capture output command and write to stdout see https://stackoverflow.com/a/16292136 + output=$(java -jar /opt/ets-ogcapi-tiles10-aio.jar "$@"|tee /dev/fd/5) +else + output=$(java -jar /opt/ets-ogcapi-tiles10-aio.jar "$@") +fi + +output_dir=$(grep "Test results" <<< "$output" | cut -d: -f3 | xargs dirname) +python3 /src/parse-results.py "${output_dir}" --service-url "${service_url}" ${EXIT_ON_FAIL} ${PRETTY_PRINT} +