Skip to content

Commit

Permalink
feat: mlserver-xgboost rock (#22)
Browse files Browse the repository at this point in the history
* feat: mlserver-xgboost rock
canonical/seldon-core-operator#133

Summary of changes:
- mlserver-xgboost rockcraft
- Unit tests.
- tox.ini ready for integration tests.

---------

Co-authored-by: Andrew Scribner <[email protected]>
  • Loading branch information
i-chvets and ca-scribner authored Jul 10, 2023
1 parent 99f993d commit 8872e41
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 0 deletions.
115 changes: 115 additions & 0 deletions mlserver-xgboost/rockcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Based on https://github.com/SeldonIO/MLServer/blob/1.2.0/Dockerfile
name: mlserver-xgboost
summary: An image for Seldon MLServer XGBoost
description: |
This image is used as part of the Charmed Kubeflow product.
version: 1.2.0_22.04_1 # <upstream-version>_<base-version>_<Charmed-KF-version>
license: Apache-2.0
base: ubuntu:22.04
run-user: _daemon_
services:
mlserver-xgboost:
override: replace
summary: "mlserver-xgboost service"
startup: enabled
# We need to build and activate the "hot-loaded" environment before MLServer starts
command: bash -c 'export PATH=/opt/conda/bin/:/opt/mlserver/.local/bin:${PATH} && \
export PYTHONPATH=/opt/mlserver/.local/lib/python3.8/site-packages/:${PYTHONPATH} && \
source /opt/conda/etc/profile.d/conda.sh && \
source /hack/activate-env.sh $MLSERVER_ENV_TARBALL && \
mlserver start $MLSERVER_MODELS_DIR'
environment:
MLSERVER_ENV_TARBALL: "/mnt/models"
MLSERVER_MODELS_DIR: "/mnt/models/environment.tar.gz"
platforms:
amd64:

parts:
mlserver-xgboost:
plugin: nil
source: https://github.com/SeldonIO/MLServer
source-type: git
source-tag: 1.2.0
build-packages:
- bash
- tar
- gzip
# TO-DO: Verify need for the packages below
# - libgomp
# - mesa-libGL
# - glib2-devel
# - shadow-utils
stage-packages:
- bash
override-build: |
mkdir -p ${CRAFT_PART_INSTALL}/opt/mlserver/dist
cp ${CRAFT_PART_SRC}/setup.py .
cp ${CRAFT_PART_SRC}/README.md .
./hack/build-wheels.sh ${CRAFT_PART_INSTALL}/opt/mlserver/dist
override-stage: |
export ROCK_RUNTIME="xgboost"
export PYTHON_VERSION="3.8.13"
export CONDA_VERSION="4.13.0"
export RUNTIMES="mlserver_${ROCK_RUNTIME}"
export MINIFORGE_VERSION="${CONDA_VERSION}-1"
export MLSERVER_PATH=opt/mlserver
export CONDA_PATH=opt/conda
export PATH=/opt/mlserver/.local/bin:/opt/conda/bin:$PATH
# Install Conda, Python 3.8 and FFmpeg
curl -L -o ~/miniforge3.sh https://github.com/conda-forge/miniforge/releases/download/${MINIFORGE_VERSION}/Miniforge3-${MINIFORGE_VERSION}-Linux-x86_64.sh
bash ~/miniforge3.sh -b -u -p ${CONDA_PATH}
rm ~/miniforge3.sh
${CONDA_PATH}/bin/conda install --yes conda=${CONDA_VERSION} python=${PYTHON_VERSION} ffmpeg
${CONDA_PATH}/bin/conda clean -tipy
mkdir -p etc/profile.d
ln -sf ${CONDA_PATH}/etc/profile.d/conda.sh etc/profile.d/conda.sh
echo ". ${CONDA_PATH}/etc/profile.d/conda.sh" >> ~/.bashrc
echo "PATH=${PATH}" >> ~/.bashrc
bash -c "${CONDA_PATH}/bin/conda init bash"
echo "conda activate base" >> ~/.bashrc
chgrp -R root opt/conda && chmod -R g+rw opt/conda
# conda writes shebangs with its path everywhere, and in crafting, that will be, for example:
# #!/root/stage/opt/conda/...
#
# Snip off the /root/stage part
bash -c "grep -R -E '/root/stage' opt/ 2>/dev/null | grep -v Bin | awk '{split(\$0,out,\":\"); print out[1]}' | uniq | xargs -I{} sed -i -e 's/\/root\/stage//' {}"
# install required wheels
mkdir -p $MLSERVER_PATH
mkdir -p ./wheels
cp -p ${CRAFT_PART_INSTALL}/opt/mlserver/dist/mlserver-*.whl ./wheels
cp -p ${CRAFT_PART_INSTALL}/opt/mlserver/dist/mlserver_${ROCK_RUNTIME}-*.whl ./wheels
pip install --prefix ${MLSERVER_PATH}/.local $(ls "./wheels/mlserver-"*.whl)
pip install --prefix ${MLSERVER_PATH}/.local $(ls "./wheels/mlserver_${ROCK_RUNTIME}-"*.whl)
# replace first line of mlserver script with reference to installed Conda python
export CONDA_PYTHON="#\!\/opt\/conda\/bin\/python"
sed -i "1s/.*/${CONDA_PYTHON}/" ${MLSERVER_PATH}/.local/bin/mlserver
# clean wheels
rm -rf ./wheels
override-prime: |
# copy all artifacts
cp -rp ${CRAFT_STAGE}/opt .
# copy required files
mkdir -p licenses
cp ${CRAFT_PART_SRC}/licenses/license.txt licenses/
mkdir -p hack
cp ${CRAFT_PART_SRC}/hack/build-env.sh hack/
cp ${CRAFT_PART_SRC}/hack/generate_dotenv.py hack/
cp ${CRAFT_PART_SRC}/hack/activate-env.sh hack/
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
47 changes: 47 additions & 0 deletions mlserver-xgboost/tests/test_rock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright 2022 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 pytest_operator.plugin import OpsTest
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(ops_test: OpsTest, rock_test_env):
"""Test rock."""
temp_dir, container_name = rock_test_env
check_rock = CheckRock("rockcraft.yaml")
rock_image = check_rock.get_image_name()
rock_version = check_rock.get_version()
LOCAL_ROCK_IMAGE = f"{check_rock.get_image_name()}:{check_rock.get_version()}"

# TO-DO uncomment when updated chisme is published
#rock_services = check_rock.get_services()
#assert rock_services["mlserver-xgboost"]
#assert rock_services["mlserver-xgboost"]["startup"] == "enabled"

# create ROCK filesystem
subprocess.run(["docker", "run", LOCAL_ROCK_IMAGE, "exec", "ls", "-la", "/opt/mlserver/.local/lib/python3.8/site-packages/mlserver"], check=True)
subprocess.run(["docker", "run", LOCAL_ROCK_IMAGE, "exec", "ls", "-la", "/opt/mlserver/.local/bin/mlserver"], check=True)
76 changes: 76 additions & 0 deletions mlserver-xgboost/tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
[tox]
skipsdist = True
skip_missing_interpreters = True

[testenv]
setenv =
PYTHONPATH={toxinidir}
PYTHONBREAKPOINT=ipdb.set_trace
CHARM_REPO=https://github.com/canonical/seldon-core-operator.git
CHARM_BRANCH=main
LOCAL_CHARM_DIR=charm_repo

[testenv:unit]
passenv = *
allowlist_externals =
bash
tox
rockcraft
deps =
juju~=2.9.0
pytest
pytest-operator
ops
charmed_kubeflow_chisme
commands =
# build and pack rock
rockcraft pack
bash -c 'NAME=$(yq eval .name rockcraft.yaml) && \
VERSION=$(yq eval .version rockcraft.yaml) && \
ARCH=$(yq eval ".platforms | keys" rockcraft.yaml | awk -F " " '\''{ print $2 }'\'') && \
ROCK="$\{NAME\}_$\{VERSION\}_$\{ARCH\}" && \
sudo skopeo --insecure-policy copy oci-archive:$ROCK.rock docker-daemon:$ROCK:$VERSION'
# run rock tests
pytest -vvv --tb native --show-capture=all --log-cli-level=INFO {posargs} {toxinidir}/tests
[testenv:integration]
passenv = *
allowlist_externals =
bash
git
rm
tox
rockcraft
sed
deps =
juju~=2.9.0
pytest
pytest-operator
ops
commands =
# build and pack rock
rockcraft pack
# clone related charm
rm -rf {env:LOCAL_CHARM_DIR}
git clone --branch {env:CHARM_BRANCH} {env:CHARM_REPO} {env:LOCAL_CHARM_DIR}
# replace jinja2 templated value with yq safe placeholder
sed -i "s/namespace: {{ namespace }}/namespace: YQ_SAFE/" {env:LOCAL_CHARM_DIR}/src/templates/configmap.yaml.j2
# upload rock to docker and microk8s cache, replace charm's container with local rock reference
bash -c 'NAME=$(yq eval .name rockcraft.yaml) && \
VERSION=$(yq eval .version rockcraft.yaml) && \
ARCH=$(yq eval ".platforms | keys" rockcraft.yaml | awk -F " " '\''{ print $2 }'\'') && \
ROCK="$\{NAME\}_$\{VERSION\}_$\{ARCH\}" && \
sudo skopeo --insecure-policy copy oci-archive:$ROCK.rock docker-daemon:$ROCK:$VERSION && \
docker save $ROCK > $ROCK.tar && \
microk8s ctr image import $ROCK.tar --digests=true && \
predictor_servers=$(yq e ".data.predictor_servers" {env:LOCAL_CHARM_DIR}/src/templates/configmap.yaml.j2) && \
predictor_servers=$(jq --arg jq_rock $ROCK -r '\''.XGBOOST_SERVER.protocols.v2.image=$jq_rock'\'' <<< $predictor_servers) && \
predictor_servers=$(jq --arg jq_version $VERSION -r '\''.XGBOOST_SERVER.protocols.v2.defaultImageVersion=$jq_version'\'' <<< $predictor_servers) yq e -i ".data.predictor_servers=strenv(predictor_servers)" {env:LOCAL_CHARM_DIR}/src/templates/configmap.yaml.j2'
# replace yq safe placeholder with original value
sed -i "s/namespace: YQ_SAFE/namespace: {{ namespace }}/" {env:LOCAL_CHARM_DIR}/src/templates/configmap.yaml.j2
# run charm integration test with rock
tox -c {env:LOCAL_CHARM_DIR} -e integration

0 comments on commit 8872e41

Please sign in to comment.