# Copyright 2019 Atalaya Tech, Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import importlib
import os
import shutil
import stat
import logging

from setuptools import sandbox

from bentoml.configuration import _is_pypi_release

from bentoml.exceptions import BentoMLException
from bentoml.saved_bundle.py_module_utils import copy_used_py_modules
from bentoml.saved_bundle.templates import (
    BENTO_SERVICE_BUNDLE_SETUP_PY_TEMPLATE,
    MANIFEST_IN_TEMPLATE,
    MODEL_SERVER_DOCKERFILE_CPU,
    INIT_PY_TEMPLATE,
)
from bentoml.utils.usage_stats import track_save
from bentoml.saved_bundle.config import SavedBundleConfig


DEFAULT_SAVED_BUNDLE_README = """\
# Generated BentoService bundle - {}:{}

This is a ML Service bundle created with BentoML, it is not recommended to edit
code or files contained in this directory. Instead, edit the code that uses BentoML
to create this bundle, and save a new BentoService bundle.
"""

logger = logging.getLogger(__name__)


def save_to_dir(bento_service, path, version=None, silent=False):
    """Save given BentoService along with all its artifacts, source code and
    dependencies to target file path, assuming path exist and empty. If target path
    is not empty, this call may override existing files in the given path.

    :param bento_service (bentoml.service.BentoService): a Bento Service instance
    :param path (str): Destination of where the bento service will be saved
    :param version (str): Override the service version with given version string
    :param silent (boolean): whether to hide the log message showing target save path
    """
    track_save(bento_service)

    from bentoml.service import BentoService

    if not isinstance(bento_service, BentoService):
        raise BentoMLException(
            "save_to_dir only work with instance of custom BentoService class"
        )

    if version is not None:
        # If parameter version provided, set bento_service version
        # Otherwise it will bet set the first time the `version` property get accessed
        bento_service.set_version(version)

    if not os.path.exists(path):
        raise BentoMLException("Directory '{}' not found".format(path))

    for artifact in bento_service._artifacts:
        if artifact.name not in bento_service._packed_artifacts:
            logger.warning(
                "Missing declared artifact '%s' for BentoService '%s'",
                artifact.name,
                bento_service.name,
            )

    module_base_path = os.path.join(path, bento_service.name)
    try:
        os.mkdir(module_base_path)
    except FileExistsError:
        raise BentoMLException(
            f"Existing module file found for BentoService {bento_service.name}"
        )

    # write README.md with custom BentoService's docstring if presented
    saved_bundle_readme = DEFAULT_SAVED_BUNDLE_README.format(
        bento_service.name, bento_service.version
    )
    if bento_service.__class__.__doc__:
        saved_bundle_readme += "\n"
        saved_bundle_readme += bento_service.__class__.__doc__.strip()

    with open(os.path.join(path, "README.md"), "w") as f:
        f.write(saved_bundle_readme)

    # save all model artifacts to 'base_path/name/artifacts/' directory
    if bento_service.artifacts:
        bento_service.artifacts.save(module_base_path)

    # write conda environment, requirement.txt
    bento_service.env.save(path, bento_service)

    # TODO: add bentoml.find_packages helper for more fine grained control over this
    # process, e.g. packages=find_packages(base, [], exclude=[], used_module_only=True)
    # copy over all custom model code
    module_name, module_file = copy_used_py_modules(
        bento_service.__class__.__module__, os.path.join(path, bento_service.name)
    )

    # create __init__.py
    with open(os.path.join(path, bento_service.name, "__init__.py"), "w") as f:
        f.write(
            INIT_PY_TEMPLATE.format(
                service_name=bento_service.name,
                module_name=module_name,
                pypi_package_version=bento_service.version,
            )
        )

    # write setup.py, this make saved BentoService bundle pip installable
    setup_py_content = BENTO_SERVICE_BUNDLE_SETUP_PY_TEMPLATE.format(
        name=bento_service.name,
        pypi_package_version=bento_service.version,
        long_description=saved_bundle_readme,
    )
    with open(os.path.join(path, "setup.py"), "w") as f:
        f.write(setup_py_content)

    with open(os.path.join(path, "MANIFEST.in"), "w") as f:
        f.write(MANIFEST_IN_TEMPLATE.format(service_name=bento_service.name))

    # write Dockerfile
    with open(os.path.join(path, "Dockerfile"), "w") as f:
        f.write(
            MODEL_SERVER_DOCKERFILE_CPU.format(
                docker_base_image=bento_service._env._docker_base_image
            )
        )

    # Copy docker-entrypoint.sh
    docker_entrypoint_sh_file_src = os.path.join(
        os.path.dirname(__file__), "docker-entrypoint.sh"
    )
    docker_entrypoint_sh_file_dst = os.path.join(path, "docker-entrypoint.sh")
    shutil.copyfile(docker_entrypoint_sh_file_src, docker_entrypoint_sh_file_dst)
    # chmod +x docker-entrypoint.sh
    st = os.stat(docker_entrypoint_sh_file_dst)
    os.chmod(docker_entrypoint_sh_file_dst, st.st_mode | stat.S_IEXEC)

    # copy bentoml-init.sh for install targz bundles
    bentoml_init_sh_file_src = os.path.join(
        os.path.dirname(__file__), "bentoml-init.sh"
    )
    bentoml_init_sh_file_dst = os.path.join(path, "bentoml-init.sh")
    shutil.copyfile(bentoml_init_sh_file_src, bentoml_init_sh_file_dst)
    # chmod +x bentoml_init_script file
    st = os.stat(bentoml_init_sh_file_dst)
    os.chmod(bentoml_init_sh_file_dst, st.st_mode | stat.S_IEXEC)

    # write bentoml.yml
    config = SavedBundleConfig(bento_service)
    config["metadata"].update({"module_name": module_name, "module_file": module_file})

    config.write_to_path(path)
    # Also write bentoml.yml to module base path to make it accessible
    # as package data after pip installed as a python package
    config.write_to_path(module_base_path)

    bundled_pip_dependencies_path = os.path.join(path, 'bundled_pip_dependencies')
    _bundle_local_bentoml_if_installed_from_source(bundled_pip_dependencies_path)

    if not silent:
        logger.info(
            "BentoService bundle '%s:%s' created at: %s",
            bento_service.name,
            bento_service.version,
            path,
        )


def _bundle_local_bentoml_if_installed_from_source(target_path):
    """
    if bentoml is installed in editor mode(pip install -e), this will build a source
    distribution with the local bentoml fork and add it to saved BentoService bundle
    path under bundled_pip_dependencies directory
    """

    # Find bentoml module path
    (module_location,) = importlib.util.find_spec('bentoml').submodule_search_locations

    bentoml_setup_py = os.path.abspath(os.path.join(module_location, '..', 'setup.py'))

    # this is for BentoML developer to create BentoService containing custom develop
    # branches of BentoML library, it is True only when BentoML module is installed in
    # development mode via "pip install --editable ."
    if not _is_pypi_release() and os.path.isfile(bentoml_setup_py):
        logger.info(
            "Detect BentoML installed in development model, copying local BentoML "
            "module file to target saved bundle path"
        )

        # Create tmp directory inside bentoml module for storing the bundled
        # targz file. Since dist-dir can only be inside of the module directory
        bundle_dir_name = '__bentoml_tmp_sdist_build'
        source_dir = os.path.abspath(
            os.path.join(module_location, '..', bundle_dir_name)
        )

        if os.path.isdir(source_dir):
            shutil.rmtree(source_dir, ignore_errors=True)
        os.mkdir(source_dir)

        sandbox.run_setup(
            bentoml_setup_py,
            ['sdist', '--format', 'gztar', '--dist-dir', bundle_dir_name],
        )

        # copy the generated targz to saved bundle directory and remove it from
        # bentoml module directory
        shutil.copytree(source_dir, target_path)

        # clean up sdist build files
        shutil.rmtree(source_dir)

bentoml/saved_bundle/bundler.py
# Copyright 2019 Atalaya Tech, Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import importlib
import os
import shutil
import stat
import logging

from setuptools import sandbox

from bentoml.configuration import _is_pypi_release

from bentoml.exceptions import BentoMLException
from bentoml.saved_bundle.py_module_utils import copy_used_py_modules
from bentoml.saved_bundle.templates import (
    BENTO_SERVICE_BUNDLE_SETUP_PY_TEMPLATE,
    MANIFEST_IN_TEMPLATE,
    MODEL_SERVER_DOCKERFILE_CPU,
    INIT_PY_TEMPLATE,
)
from bentoml.utils.usage_stats import track_save
from bentoml.saved_bundle.config import SavedBundleConfig


DEFAULT_SAVED_BUNDLE_README = """\
# Generated BentoService bundle - {}:{}

This is a ML Service bundle created with BentoML, it is not recommended to edit
code or files contained in this directory. Instead, edit the code that uses BentoML
to create this bundle, and save a new BentoService bundle.
"""

logger = logging.getLogger(__name__)


def save_to_dir(bento_service, path, version=None, silent=False):
    """Save given BentoService along with all its artifacts, source code and
    dependencies to target file path, assuming path exist and empty. If target path
    is not empty, this call may override existing files in the given path.

    :param bento_service (bentoml.service.BentoService): a Bento Service instance
    :param path (str): Destination of where the bento service will be saved
    :param version (str): Override the service version with given version string
    :param silent (boolean): whether to hide the log message showing target save path
    """
    track_save(bento_service)

    from bentoml.service import BentoService

    if not isinstance(bento_service, BentoService):
        raise BentoMLException(
            "save_to_dir only work with instance of custom BentoService class"
        )

    if version is not None:
        # If parameter version provided, set bento_service version
        # Otherwise it will bet set the first time the `version` property get accessed
        bento_service.set_version(version)

    if not os.path.exists(path):
        raise BentoMLException("Directory '{}' not found".format(path))

    for artifact in bento_service._artifacts:
        if artifact.name not in bento_service._packed_artifacts:
            logger.warning(
                "Missing declared artifact '%s' for BentoService '%s'",
                artifact.name,
                bento_service.name,
            )

    module_base_path = os.path.join(path, bento_service.name)
    try:
        os.mkdir(module_base_path)
    except FileExistsError:
        raise BentoMLException(
            f"Existing module file found for BentoService {bento_service.name}"
        )

    # write README.md with custom BentoService's docstring if presented
    saved_bundle_readme = DEFAULT_SAVED_BUNDLE_README.format(
        bento_service.name, bento_service.version
    )
    if bento_service.__class__.__doc__:
        saved_bundle_readme += "\n"
        saved_bundle_readme += bento_service.__class__.__doc__.strip()

    with open(os.path.join(path, "README.md"), "w") as f:
        f.write(saved_bundle_readme)

    # save all model artifacts to 'base_path/name/artifacts/' directory
    if bento_service.artifacts:
        bento_service.artifacts.save(module_base_path)

    # write conda environment, requirement.txt
    bento_service.env.save(path, bento_service)

    # TODO: add bentoml.find_packages helper for more fine grained control over this
    # process, e.g. packages=find_packages(base, [], exclude=[], used_module_only=True)
    # copy over all custom model code
    module_name, module_file = copy_used_py_modules(
        bento_service.__class__.__module__, os.path.join(path, bento_service.name)
    )

    # create __init__.py
    with open(os.path.join(path, bento_service.name, "__init__.py"), "w") as f:
        f.write(
            INIT_PY_TEMPLATE.format(
                service_name=bento_service.name,
                module_name=module_name,
                pypi_package_version=bento_service.version,
            )
        )

    # write setup.py, this make saved BentoService bundle pip installable
    setup_py_content = BENTO_SERVICE_BUNDLE_SETUP_PY_TEMPLATE.format(
        name=bento_service.name,
        pypi_package_version=bento_service.version,
        long_description=saved_bundle_readme,
    )
    with open(os.path.join(path, "setup.py"), "w") as f:
        f.write(setup_py_content)

    with open(os.path.join(path, "MANIFEST.in"), "w") as f:
        f.write(MANIFEST_IN_TEMPLATE.format(service_name=bento_service.name))

    # write Dockerfile
    logger.debug("Using Docker Base Image %s", bento_service._env._docker_base_image)
    with open(os.path.join(path, "Dockerfile"), "w") as f:
        f.write(
            MODEL_SERVER_DOCKERFILE_CPU.format(
                docker_base_image=bento_service._env._docker_base_image
            )
        )

    # Copy docker-entrypoint.sh
    docker_entrypoint_sh_file_src = os.path.join(
        os.path.dirname(__file__), "docker-entrypoint.sh"
    )
    docker_entrypoint_sh_file_dst = os.path.join(path, "docker-entrypoint.sh")
    shutil.copyfile(docker_entrypoint_sh_file_src, docker_entrypoint_sh_file_dst)
    # chmod +x docker-entrypoint.sh
    st = os.stat(docker_entrypoint_sh_file_dst)
    os.chmod(docker_entrypoint_sh_file_dst, st.st_mode | stat.S_IEXEC)

    # copy bentoml-init.sh for install targz bundles
    bentoml_init_sh_file_src = os.path.join(
        os.path.dirname(__file__), "bentoml-init.sh"
    )
    bentoml_init_sh_file_dst = os.path.join(path, "bentoml-init.sh")
    shutil.copyfile(bentoml_init_sh_file_src, bentoml_init_sh_file_dst)
    # chmod +x bentoml_init_script file
    st = os.stat(bentoml_init_sh_file_dst)
    os.chmod(bentoml_init_sh_file_dst, st.st_mode | stat.S_IEXEC)

    # write bentoml.yml
    config = SavedBundleConfig(bento_service)
    config["metadata"].update({"module_name": module_name, "module_file": module_file})

    config.write_to_path(path)
    # Also write bentoml.yml to module base path to make it accessible
    # as package data after pip installed as a python package
    config.write_to_path(module_base_path)

    bundled_pip_dependencies_path = os.path.join(path, 'bundled_pip_dependencies')
    _bundle_local_bentoml_if_installed_from_source(bundled_pip_dependencies_path)

    if not silent:
        logger.info(
            "BentoService bundle '%s:%s' created at: %s",
            bento_service.name,
            bento_service.version,
            path,
        )


def _bundle_local_bentoml_if_installed_from_source(target_path):
    """
    if bentoml is installed in editor mode(pip install -e), this will build a source
    distribution with the local bentoml fork and add it to saved BentoService bundle
    path under bundled_pip_dependencies directory
    """

    # Find bentoml module path
    (module_location,) = importlib.util.find_spec('bentoml').submodule_search_locations

    bentoml_setup_py = os.path.abspath(os.path.join(module_location, '..', 'setup.py'))

    # this is for BentoML developer to create BentoService containing custom develop
    # branches of BentoML library, it is True only when BentoML module is installed in
    # development mode via "pip install --editable ."
    if not _is_pypi_release() and os.path.isfile(bentoml_setup_py):
        logger.info(
            "Detect BentoML installed in development model, copying local BentoML "
            "module file to target saved bundle path"
        )

        # Create tmp directory inside bentoml module for storing the bundled
        # targz file. Since dist-dir can only be inside of the module directory
        bundle_dir_name = '__bentoml_tmp_sdist_build'
        source_dir = os.path.abspath(
            os.path.join(module_location, '..', bundle_dir_name)
        )

        if os.path.isdir(source_dir):
            shutil.rmtree(source_dir, ignore_errors=True)
        os.mkdir(source_dir)

        sandbox.run_setup(
            bentoml_setup_py,
            ['sdist', '--format', 'gztar', '--dist-dir', bundle_dir_name],
        )

        # copy the generated targz to saved bundle directory and remove it from
        # bentoml module directory
        shutil.copytree(source_dir, target_path)

        # clean up sdist build files
        shutil.rmtree(source_dir)

bentoml/saved_bundle/bundler.py
diff --git a/bentoml/saved_bundle/bundler.py b/bentoml/saved_bundle/bundler.py
index df0d47d819c..dafa6b73877 100644
--- a/bentoml/saved_bundle/bundler.py
+++ b/bentoml/saved_bundle/bundler.py
@@ -135,6 +135,7 @@ def save_to_dir(bento_service, path, version=None, silent=False):
         f.write(MANIFEST_IN_TEMPLATE.format(service_name=bento_service.name))
 
     # write Dockerfile
+    logger.debug("Using Docker Base Image %s", bento_service._env._docker_base_image)
     with open(os.path.join(path, "Dockerfile"), "w") as f:
         f.write(
             MODEL_SERVER_DOCKERFILE_CPU.format(
diff --git a/docker/model-server/Dockerfile-alpine b/docker/model-server/Dockerfile-alpine
index 5e598f322af..61a226c2e5d 100644
--- a/docker/model-server/Dockerfile-alpine
+++ b/docker/model-server/Dockerfile-alpine
@@ -1,28 +1,26 @@
-FROM continuumio/miniconda3:4.8.2-alpine
-
-# Install all build dependencies and bash for user setup scripts
-USER root
-RUN apk update \
-    && apk add --virtual build-dependencies \
-        build-base \
-        gcc \
-        wget \
-        git \
-        linux-headers \
-    && apk add \
-        bash
-
-# Restore USER as set in base image
-# https://github.com/ContinuumIO/docker-images/blob/2020_02_release/miniconda3/alpine/Dockerfile
-USER 10151
+ARG PYTHON_VERSION
+FROM python:${PYTHON_VERSION}-slim AS compile-image
+RUN apt-get update
+RUN apt-get install -y --no-install-recommends build-essential gcc
 
 ARG BENTOML_VERSION
 ENV BENTOML_VERSION=$BENTOML_VERSION
 
-# pre-install BentoML base dependencies
-RUN sh --login -i -c "pip install bentoml==$BENTOML_VERSION"
+RUN python -m venv /opt/venv
+# Make sure we use the virtualenv:
+ENV PATH="/opt/venv/bin:$PATH"
+
+# pre-install BentoML requirements
+RUN pip install bentoml==$BENTOML_VERSION
+COPY entrypoint.sh .
+
+# multi-stage build to shave dependency cost
+FROM python:${PYTHON_VERSION}-slim AS build-image
+COPY --from=compile-image /opt/venv /opt/venv
+COPY --from=compile-image entrypoint.sh /usr/local/bin
 
-COPY entrypoint.sh /usr/local/bin/
+# Make sure we use the virtualenv:
+ENV PATH="/opt/venv/bin:$PATH"
 
 ENTRYPOINT [ "entrypoint.sh" ]
-CMD ["bentoml", "serve-gunicorn", "/bento"]
+CMD ["bentoml", "serve-gunicorn", "/bento"]
\ No newline at end of file
diff --git a/docker/model-server/release.sh b/docker/model-server/release.sh
index 113eea7ac19..176d62b2cb7 100755
--- a/docker/model-server/release.sh
+++ b/docker/model-server/release.sh
@@ -8,7 +8,6 @@ else
   exit 1
 fi
 
-
 GIT_ROOT=$(git rev-parse --show-toplevel)
 cd "$GIT_ROOT"/docker/model-server
 
@@ -19,13 +18,20 @@ docker build --pull \
     .
 docker push bentoml/model-server:"$BENTOML_VERSION"
 
-echo "Releasing alpine based docker base image.."
-docker build --pull \
-    --build-arg BENTOML_VERSION="$BENTOML_VERSION" \
-    -t bentoml/model-server:"$BENTOML_VERSION"-alpine \
+PYTHON_MAJOR_VERSIONS=(3.6 3.7 3.8)
+echo "Building Alpine based docker base images for ${PYTHON_MAJOR_VERSIONS[*]}"
+for version in "${PYTHON_MAJOR_VERSIONS[@]}"
+do
+    echo "Releasing Alpine based docker base image for Python $version.."
+    docker build --pull \
+    --build-arg BENTOML_VERSION=$BENTOML_VERSION \
+    --build-arg PYTHON_VERSION=$version \
+    -t bentoml/model-server:$BENTOML_VERSION-alpine-py${version//.} \
     -f Dockerfile-alpine \
     --network=host \
     .
-docker push bentoml/model-server:"$BENTOML_VERSION"-alpine
 
+    docker push bentoml/model-server:$BENTOML_VERSION-alpine-py${version//.}
+
+done
 echo "Done"
diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst
index 3ec31d8c2b0..94169d98daf 100644
--- a/docs/source/concepts.rst
+++ b/docs/source/concepts.rst
@@ -214,6 +214,53 @@ on an H2O model that requires the h2o conda packages:
     One caveat with Conda Packages here, is that it does not work with AWS Lambda 
     deployment due to the limitation of the AWS Lambda platform.
 
+Using other Docker base images
+^^^^^^^^^^^^^^^^
+
+By default, BentoML uses a default Docker base image and installs your model and its dependencies on top of it. This base image contains all of BentoML's dependencies and an installation of `conda` which helps BentoML to package and use the right Python version for your bundle.
+
+However, there may be times when you need to use other Docker images (e.g. have some pre-build dependencies layers, company base image, using an Alpine-based image, etc.). BentoML makes it really easy to switch between base images by doing allowing you to specify a `docker_base_image`.
+
+.. code-block:: python
+
+  # e.g. using BentoML Alpine-based images
+  @env(docker_base_image="bentoml/model-server:0.8.12-alpine-py37")
+  @artifacts([SklearnModelArtifact('model')])
+  class ExamplePredictionService(BentoService):
+    ...
+
+In fact, one such base image that many may find useful are the Alpine-based images. The original base image weighs in at roughly `~853MB` whereas the Alpine version weighs in at `~360MB`. 
+
+.. code-block:: bash
+
+  > docker image ls
+
+  REPOSITORY                             TAG                   IMAGE ID            CREATED              SIZE
+  bentoml/model-server                   0.8.12-alpine-py37    109b451ed537        6 minutes ago        360MB
+  bentoml/model-server                   0.8.12                f034fa23264c        33 minutes ago       853MB
+
+This means that each image built on top of Alpine based images will be significantly smaller. 
+
+.. code-block:: bash
+
+  > docker image ls
+
+  REPOSITORY                               TAG                 IMAGE ID            CREATED              SIZE
+  jzhao2k19/iris                           latest              bfc9b81c7535        About a minute ago   1.54GB
+  jzhao2k19/iris-alpine                    latest              4e8d87a0c18a        4 minutes ago        577MB
+
+However, as with using any alternative Docker base image, there are a few things to keep in mind. The regular base image uses `conda`, whereas the Alpine image does not. This has a few consequences. BentoML uses `conda` to ensure the Python version used matches the one you used to save your bundle. This means that you should manually select the right Alpine base image for your bundle. For example, if you used Python 3.8 to train your model, you would use `bentoml/model-server:0.8.12-alpine-py38`. Currently, BentoML support Python 3.6, 3.7, and 3.8. Additionally, this means that BentoML will ignore the `environment.yml`, meaning that user-defined `conda` packages and dependencies will be ignored. In the following example, only `pandas` will be installed, as the `conda_channels` and `conda_dependencies` will be ignored.
+
+.. code-block:: python
+
+  @bentoml.env(
+    pip_dependencies=['pandas'],
+    conda_channels=['h2oai'],
+    conda_dependencies=['h2o==3.24.0.2'],
+    docker_base_image="bentoml/model-server:0.8.12-alpine-py37"
+  )
+  class ExamplePredictionService(bentoml.BentoService):
+    ...
 
 Init Bash Script
 ^^^^^^^^^^^^^^^^

Add support for Alpine based docker image
**Is your feature request related to a problem? Please describe.**

Allow users to use `bentoml/model-server:0.7.8-alpine` as the base image, which is currently defined here:  https://github.com/bentoml/BentoML/blob/master/docker/model-server/Dockerfile-alpine

**Describe the solution you'd like**

Improve the `bentoml_init.sh` script to make sure it works on both debian and alpine based docker image.

**Describe alternatives you've considered**
n/a

**Additional context**

See https://github.com/bentoml/BentoML/issues/693

