Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/_bentoml_impl/server/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ def create_dependency_watcher(

# Process environment variables from the service
for env_var in svc.envs:
# Exclude Build Time environment variables
if env_var.stage == "build":
continue
if env_var.name in env:
continue

Expand Down Expand Up @@ -207,6 +210,9 @@ def serve_http(

# Process environment variables from the service
for env_var in svc.envs:
# Exclude Build Time environment variables
if env_var.stage == "build":
continue
if env_var.name in env:
continue

Expand Down Expand Up @@ -240,6 +246,9 @@ def serve_http(
# Process environment variables for dependency services
dependency_env = env.copy()
for env_var in dep_svc.envs:
# Exclude Build Time environment variables
if env_var.stage == "build":
continue
if env_var.name in dependency_env:
continue

Expand Down
11 changes: 10 additions & 1 deletion src/_bentoml_sdk/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from bentoml._internal.configuration import get_bentoml_requirement
from bentoml._internal.configuration import get_debug_mode
from bentoml._internal.configuration import get_quiet_mode
from bentoml._internal.container import split_envs_by_stage
from bentoml._internal.container.frontend.dockerfile import CONTAINER_METADATA
from bentoml._internal.container.frontend.dockerfile import CONTAINER_SUPPORTED_DISTROS
from bentoml.exceptions import BentoMLConfigException
Expand All @@ -25,6 +26,7 @@
if t.TYPE_CHECKING:
from bentoml._internal.bento.build_config import BentoEnvSchema


if sys.version_info >= (3, 11):
import tomllib
else:
Expand Down Expand Up @@ -196,8 +198,15 @@ def freeze(
docker_folder = bento_fs.joinpath("env", "docker")
docker_folder.mkdir(parents=True, exist_ok=True)
dockerfile_path = docker_folder.joinpath("Dockerfile")
runtime_envs, build_stage_envs = split_envs_by_stage(envs)
dockerfile_path.write_text(
generate_dockerfile(info, bento_fs, enable_buildkit=False, envs=envs),
generate_dockerfile(
info,
bento_fs,
enable_buildkit=bool(build_stage_envs),
envs=runtime_envs,
secret_envs=build_stage_envs,
),
)
for script_name, target_path in self.scripts.items():
shutil.copy(script_name, bento_fs / target_path)
Expand Down
12 changes: 9 additions & 3 deletions src/_bentoml_sdk/service/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ class PathMetadata(t.TypedDict):
mounted: bool


class ServiceEnvConfig(t.TypedDict, total=False):
name: str
value: str
stage: t.Literal["all", "build", "runtime"]


def with_config(
func: t.Callable[t.Concatenate["Service[t.Any]", P], R],
) -> t.Callable[t.Concatenate["Service[t.Any]", P], R]:
Expand All @@ -70,7 +76,7 @@ def wrapper(self: Service[t.Any], *args: P.args, **kwargs: P.kwargs) -> R:
return wrapper


def convert_envs(envs: t.List[t.Dict[str, t.Any]]) -> t.List[BentoEnvSchema]:
def convert_envs(envs: t.List[ServiceEnvConfig]) -> t.List[BentoEnvSchema]:
return [BentoEnvSchema(**env) for env in envs]


Expand Down Expand Up @@ -538,7 +544,7 @@ def service(
name: str | None = None,
image: Image | None = None,
description: str | None = None,
envs: list[dict[str, str]] | None = None,
envs: list[ServiceEnvConfig] | None = None,
labels: dict[str, str] | None = None,
cmd: list[str] | None = None,
service_class: type[Service[T]] = Service,
Expand All @@ -553,7 +559,7 @@ def service(
name: str | None = None,
image: Image | None = None,
description: str | None = None,
envs: list[dict[str, str]] | None = None,
envs: list[ServiceEnvConfig] | None = None,
labels: dict[str, str] | None = None,
cmd: list[str] | None = None,
service_class: type[Service[T]] = Service,
Expand Down
7 changes: 7 additions & 0 deletions src/bentoml/_internal/bento/build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,11 +772,18 @@ def _model_spec_structure_hook(
return cls.from_item(d)


EnvStage = t.Literal["all", "build", "runtime"]


@attr.define(eq=True)
class BentoEnvSchema:
__forbid_extra_keys__ = False
name: str
value: str = ""
stage: EnvStage = attr.field(
default="all",
validator=attr.validators.in_(("all", "build", "runtime")),
)


bentoml_cattr.register_structure_hook(ModelSpec, _model_spec_structure_hook)
Expand Down
10 changes: 8 additions & 2 deletions src/bentoml/_internal/cloud/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,9 @@ def verify(
default_envs = [
{"name": env.name, "value": env.value}
for env in manifest.envs
if env.value and env.name not in existing_env_names
if env.value
and env.name not in existing_env_names
and env.stage != "build" # Exclude build time only env
]
if default_envs:
if self.envs is None:
Expand All @@ -206,7 +208,11 @@ def verify(
# these defaults to BentoCloud.
self.cfg_dict["envs"] = self.envs

required_envs = [env.name for env in manifest.envs if not env.value]
required_envs = [
env.name
for env in manifest.envs
if env.stage != "build" and not env.value
]
provided_envs: list[str] = [env["name"] for env in (self.envs or [])]
if self.secrets:
secret_api = SecretAPI(_client)
Expand Down
59 changes: 57 additions & 2 deletions src/bentoml/_internal/container/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from simple_di import Provide
from simple_di import inject

from ...exceptions import BentoMLException
from ...exceptions import InvalidArgument
from ..configuration.containers import BentoMLContainer
from ..utils.cattr import bentoml_cattr
Expand All @@ -24,6 +25,7 @@
if TYPE_CHECKING:
from ..bento import Bento
from ..bento import BentoStore
from ..bento.build_config import BentoEnvSchema
from ..tag import Tag
from .base import Arguments

Expand Down Expand Up @@ -112,6 +114,19 @@ def enable_buildkit(
raise ValueError("Either backend or builder must be provided.")


def split_envs_by_stage(
envs: t.Sequence[BentoEnvSchema],
) -> tuple[list[BentoEnvSchema], list[BentoEnvSchema]]:
runtime_envs: list[BentoEnvSchema] = []
build_envs: list[BentoEnvSchema] = []
for env in envs:
if env.stage == "build":
build_envs.append(env)
else:
runtime_envs.append(env)
return runtime_envs, build_envs


# XXX: Sync with BentoML extra dependencies found in pyproject.toml
FEATURES = frozenset(
{
Expand Down Expand Up @@ -216,12 +231,19 @@ def construct_containerfile(
from _bentoml_impl.docker import generate_dockerfile

assert isinstance(options, BentoInfoV2)
runtime_envs, build_env = split_envs_by_stage(options.envs)
if build_env and not enable_buildkit:
raise BentoMLException(
"stage='build' environment variables require BuildKit. "
"Enable BuildKit for your backend (e.g. set DOCKER_BUILDKIT=1)."
)
dockerfile = generate_dockerfile(
options.image,
Path(tempdir),
enable_buildkit=enable_buildkit,
add_header=add_header,
envs=options.envs,
envs=runtime_envs,
secret_envs=build_env,
)
instruction.append(dockerfile)
Path(tempdir, dockerfile_path).write_text("\n".join(instruction))
Expand All @@ -241,15 +263,47 @@ def build(
bento = _bento_store.get(bento_tag)

builder = get_backend(backend)
buildkit_enabled = enable_buildkit(builder=builder)
_, build_stage_envs = split_envs_by_stage(bento.info.envs)
secret_specs: list[str] = []
if build_stage_envs:
if not buildkit_enabled:
raise BentoMLException(
"stage='build' environment variables require BuildKit. "
"Enable BuildKit for your backend (e.g. set DOCKER_BUILDKIT=1)."
)
for env in build_stage_envs:
secret_value = env.value or os.environ.get(env.name)
if secret_value is None:
raise BentoMLException(
f"Environment variable '{env.name}' (stage='build') is required during image build."
)
secret_file = tempfile.NamedTemporaryFile("w", delete=False)
secret_file.write(secret_value)
secret_file.flush()
secret_file.close()
clean_context.callback(lambda path=secret_file.name: os.remove(path))
secret_specs.append(f"id={env.name},src={secret_file.name}")

context_path, dockerfile = clean_context.enter_context(
construct_containerfile(
bento,
features=features,
enable_buildkit=enable_buildkit(builder=builder),
enable_buildkit=buildkit_enabled,
)
)
try:
if secret_specs:
existing_secret = kwargs.get("secret")
if existing_secret is None:
merged_secret: tuple[str, ...] = tuple(secret_specs)
elif isinstance(existing_secret, (tuple, list)):
merged_secret = (*existing_secret, *secret_specs)
else:
merged_secret = (existing_secret, *secret_specs)
kwargs["secret"] = merged_secret
kwargs.update({"file": dockerfile, "context_path": context_path})
breakpoint()
return builder.build(**kwargs)
except Exception as e: # pylint: disable=broad-except
logger.error(
Expand Down Expand Up @@ -386,4 +440,5 @@ def get_backend(backend: str) -> OCIBuilder:
"register_backend",
"get_backend",
"REGISTERED_BACKENDS",
"split_envs_by_stage",
]
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@
{% set bento__entrypoint = expands_bento_path('env', 'docker', 'entrypoint.sh', bento_path=bento__path) %}
{% set __enable_buildkit__ = bento__enable_buildkit | default(False) -%}
{% set __bento_envs__ = bento__envs | default([]) %}
{% set __bento_secret_envs__ = bento__secret_envs | default([]) %}
{% set __secret_mounts__ = "" %}
{% set __secret_exports__ = "" %}
{% if __bento_secret_envs__ and __enable_buildkit__ %}
{% set mounts = [] %}
{% for env in __bento_secret_envs__ %}
{% do mounts.append("--mount=type=secret,id=%s" % env.name) %}
{% endfor %}
{% set __secret_mounts__ = " ".join(mounts) + " " %}
{% set secret_exports = [] %}
{% for env in __bento_secret_envs__ %}
{% do secret_exports.append('%s="$(cat /run/secrets/%s)"' % (env.name, env.name)) %}
{% endfor %}
{% set __secret_exports__ = " ".join(secret_exports) + " " %}
{% endif %}

{% if __enable_buildkit__ %}
# 1.2.1 is the current docker frontend that both buildkitd and kaniko supports.
# syntax = {{ bento__buildkit_frontend }}
Expand Down Expand Up @@ -49,10 +65,15 @@ ENV BENTOML_HOME={{ bento__home }}
ENV BENTOML_HF_CACHE_DIR={{ bento__path }}/hf-models
ENV BENTOML_CONTAINERIZED=true

{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {% endcall -%} {{ __secret_exports__ }}echo "DEBUG: Listing all build secrets:" && ls -l /run/secrets || true && for s in /run/secrets/*; do echo "SECRET $s:" && cat "$s" || true; done

{% for env in __bento_envs__ %}
{% set stage = env.stage | default("all") -%}
{% if stage != "runtime" -%}
ARG {{ env.name }}{% if env.value %}={{ env.value }}{% endif %}

ENV {{ env.name }}=${{ env.name }}
{% endif -%}
{% endfor %}

RUN mkdir $BENTO_PATH && chown {{ bento__user }}:{{ bento__user }} $BENTO_PATH -R
Expand All @@ -62,10 +83,10 @@ WORKDIR $BENTO_PATH
{% block SETUP_BENTO_COMPONENTS %}
COPY --chown={{ bento__user }}:{{ bento__user }} ./env/docker ./env/docker/
{% for command in __options__commands %}
RUN {{ command }}
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {% endcall -%} {{ __secret_exports__ }}{{ command }}
{% endfor %}
RUN command -v uv >/dev/null || pip install uv
RUN UV_PYTHON_INSTALL_DIR=/app/python/ uv venv --python {{ __options__python_version }} /app/.venv && \
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {% endcall -%} {{ __secret_exports__ }}command -v uv >/dev/null || pip install uv
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {% endcall -%} {{ __secret_exports__ }}UV_PYTHON_INSTALL_DIR=/app/python/ uv venv --python {{ __options__python_version }} /app/.venv && \
chown -R {{ bento__user }}:{{ bento__user }} /app/.venv
ENV VIRTUAL_ENV=/app/.venv
ENV UV_COMPILE_BYTECODE=1
Expand All @@ -75,10 +96,10 @@ ENV PATH=/app/.venv/bin:${PATH}

COPY --chown={{ bento__user }}:{{ bento__user }} ./env/python ./env/python/
# install python packages
{% call common.RUN(__enable_buildkit__) -%} {{ __pip_cache__ }} {% endcall -%} uv --directory ./env/python/ pip install -r requirements.txt
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {{ __pip_cache__ }} {% endcall -%} {{ __secret_exports__ }}uv --directory ./env/python/ pip install -r requirements.txt

{% for command in __options__post_commands %}
RUN {{ command }}
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {% endcall -%} {{ __secret_exports__ }}{{ command }}
{% endfor %}

COPY --chown={{ bento__user }}:{{ bento__user }} . ./
Expand Down
Loading