diff --git a/xgbserver/README.md b/xgbserver/README.md new file mode 100644 index 0000000..99d0dc6 --- /dev/null +++ b/xgbserver/README.md @@ -0,0 +1,47 @@ +## Testing + +### Prerequisites + +* docker +* Google Cloud CLI tools ([installation guide](https://cloud.google.com/sdk/docs/install)) + +### Instructions + +From the [upstream usage example](https://kserve.github.io/website/master/modelserving/v1beta1/xgboost/), this rock can be tested locally using: + +Launch the server with: +``` +# download the model locally +mkdir sample_model +gsutil cp -r gs://kfserving-examples/models/xgboost/iris ./sample_model/ + +# mount the model into the container at runtime +docker run -p 8080:8080 -v $(pwd)/sample_model/iris:/mnt/models xgbserver: --model_name test_model --model_dir=/mnt/models --http_port=8080 + +``` + +Test the server with: +``` +cat <> iris-input-v2.json +{ + "inputs": [ + { + "name": "input-0", + "shape": [2, 4], + "datatype": "FP32", + "data": [ + [6.8, 2.8, 4.8, 1.4], + [6.0, 3.4, 4.5, 1.6] + ] + } + ] +} +EOF + +curl -v \ + -H "Content-Type: application/json" \ + -d @./iris-input-v2.json \ + localhost:8080/v2/models/test_model/infer +``` + +which should return the expected output described in the docs. diff --git a/xgbserver/dummy_pyproject.toml b/xgbserver/dummy_pyproject.toml new file mode 100644 index 0000000..334a95a --- /dev/null +++ b/xgbserver/dummy_pyproject.toml @@ -0,0 +1,12 @@ +[tool.poetry] +name = "workaround-for-editable-install" +version = "0.0.1" +description = "" +authors = ["none"] + +[tool.poetry.dependencies] +# This range should match that used in upstream's server pyproject.toml +python = ">=3.8,<3.12" +kserve = { path = "../python/kserve", develop = false } +xgbserver = { path = "../python/xgbserver", develop = false } + diff --git a/xgbserver/rockcraft.yaml b/xgbserver/rockcraft.yaml new file mode 100644 index 0000000..678536f --- /dev/null +++ b/xgbserver/rockcraft.yaml @@ -0,0 +1,78 @@ +# Based on https://github.com/kserve/kserve/blob/master/python/xgb.Dockerfile +# +# See ../CONTRIBUTING.md for more details about the patterns used in this rock. +# This rock is implemented with some atypical patterns due to the native of the upstream +# Dockerfile. +name: xgbserver +summary: xgboost server for Kserve deployments +description: "Kserve xgboost server" +version: "0.11.2" +license: Apache-2.0 +base: ubuntu@22.04 +platforms: + amd64: +run-user: _daemon_ +services: + xgbserver: + override: replace + summary: "xgbserver server service" + startup: enabled + command: "python -m xgbserver [ ]" +entrypoint-service: xgbserver + +parts: + security-team-requirement: + plugin: nil + override-build: | + mkdir -p ${CRAFT_PART_INSTALL}/usr/share/rocks + (echo "# os-release" && cat /etc/os-release && echo "# dpkg-query" && \ + dpkg-query --root=${CRAFT_PROJECT_DIR}/../bundles/ubuntu-22.04/rootfs/ -f '${db:Status-Abbrev},${binary:Package},${Version},${source:Package},${Source:Version}\n' -W) \ + > ${CRAFT_PART_INSTALL}/usr/share/rocks/dpkg.query + + python: + plugin: nil + source: https://github.com/kserve/kserve.git + source-subdir: python + source-tag: v0.11.2 + build-packages: + - libgomp1 + overlay-packages: + - python3.10 + # Including python3-pip here means pip also gets primed for the final rock + - python3-pip + override-build: | + # Populate the build system's python environment with all packages needed for + # the server in the final rock + + # Setup poetry + pip install poetry==1.4.0 + poetry config virtualenvs.create false + + # Install the kserve package, this specific server package, and their dependencies. + mkdir -p ./python_env_builddir + cp -rf $CRAFT_PROJECT_DIR/dummy_pyproject.toml ./python_env_builddir/pyproject.toml + (cd python_env_builddir && poetry install --no-interaction --no-root) + + # Promote the packages we've installed from the local env to the primed image + mkdir -p $CRAFT_PART_INSTALL/usr/local/lib/python3.10/dist-packages + cp -fr /usr/local/lib/python3.10/dist-packages/* $CRAFT_PART_INSTALL/usr/local/lib/python3.10/dist-packages/ + + # TODO: why do we need this? + mkdir -p $CRAFT_PART_INSTALL/usr/local/share + cp -fr /usr/local/share/* $CRAFT_PART_INSTALL/usr/local/share/ + + # Ensure `python` is an executable command in our primed image by making + # a symbolic link + mkdir -p $CRAFT_PART_INSTALL/usr/bin/ + ln -s /usr/bin/python3.10 $CRAFT_PART_INSTALL/usr/bin/python + + # Copy licenses + third-party: + plugin: nil + after: [python] + source: https://github.com/kserve/kserve.git + source-subdir: python + source-tag: v0.11.2 + override-build: | + cp -fr third_party/* ${CRAFT_PART_INSTALL}/third_party + diff --git a/xgbserver/tests/test_rock.py b/xgbserver/tests/test_rock.py new file mode 100644 index 0000000..caa84a2 --- /dev/null +++ b/xgbserver/tests/test_rock.py @@ -0,0 +1,78 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +from pathlib import Path + +import os +import logging +import random +import pytest +import string +import subprocess +import yaml + +from charmed_kubeflow_chisme.rock import CheckRock + + +@pytest.fixture() +def rock_test_env(tmpdir): + """Yields a temporary directory and random docker container name, then cleans them up after.""" + container_name = "".join( + [str(i) for i in random.choices(string.ascii_lowercase, k=8)] + ) + yield tmpdir, container_name + + try: + subprocess.run(["docker", "rm", container_name]) + except Exception: + pass + # tmpdir fixture we use here should clean up the other files for us + + +@pytest.mark.abort_on_fail +def test_rock(rock_test_env): + """Test rock.""" + temp_dir, container_name = rock_test_env + check_rock = CheckRock("rockcraft.yaml") + rock_image = check_rock.get_name() + rock_version = check_rock.get_version() + LOCAL_ROCK_IMAGE = f"{rock_image}:{rock_version}" + + # assert we have the expected files + subprocess.run( + [ + "docker", + "run", + "--entrypoint", + "/bin/bash", + LOCAL_ROCK_IMAGE, + "-c", + "ls -la /usr/local/lib/python3.10/dist-packages/xgbserver", + ], + check=True, + ) + subprocess.run( + [ + "docker", + "run", + "--entrypoint", + "/bin/bash", + LOCAL_ROCK_IMAGE, + "-c", + "ls -la /usr/local/lib/python3.10/dist-packages/kserve", + ], + check=True, + ) + subprocess.run( + [ + "docker", + "run", + "--entrypoint", + "/bin/bash", + LOCAL_ROCK_IMAGE, + "-c", + "ls -la /third_party", + ], + check=True, + ) + diff --git a/xgbserver/tox.ini b/xgbserver/tox.ini new file mode 100644 index 0000000..de23c73 --- /dev/null +++ b/xgbserver/tox.ini @@ -0,0 +1,54 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +[tox] +skipsdist = True +skip_missing_interpreters = True +envlist = pack, export-to-docker, sanity, integration + +[testenv] +setenv = + PYTHONPATH={toxinidir} + PYTHONBREAKPOINT=ipdb.set_trace + CHARM_REPO=https://github.com/canonical/kserve-operators.git + CHARM_BRANCH=main + LOCAL_CHARM_DIR=charm_repo + +[testenv:pack] +passenv = * +allowlist_externals = + rockcraft +commands = + rockcraft pack + +[testenv:export-to-docker] +passenv = * +allowlist_externals = + bash + skopeo + yq +commands = + # pack rock and export to docker + bash -c 'NAME=$(yq eval .name rockcraft.yaml) && \ + VERSION=$(yq eval .version rockcraft.yaml) && \ + ARCH=$(yq eval ".platforms | keys | .[0]" rockcraft.yaml) && \ + ROCK="$\{NAME\}_$\{VERSION\}_$\{ARCH\}.rock" && \ + DOCKER_IMAGE=$NAME:$VERSION && \ + echo "Exporting $ROCK to docker as $DOCKER_IMAGE" && \ + skopeo --insecure-policy copy oci-archive:$ROCK docker-daemon:$DOCKER_IMAGE' + +[testenv:sanity] +passenv = * +deps = + pytest + charmed-kubeflow-chisme +commands = + # run rock tests + pytest -s -v --tb native --show-capture=all --log-cli-level=INFO {posargs} {toxinidir}/tests + +[testenv:integration] +passenv = * +allowlist_externals = + echo +commands = + # TODO: Implement integration tests here + echo "WARNING: This is a placeholder test - no test is implemented here." \ No newline at end of file