diff --git a/Dockerfile.QA b/Dockerfile.QA index 6560b2c5e6..8354d9aeb4 100644 --- a/Dockerfile.QA +++ b/Dockerfile.QA @@ -52,7 +52,8 @@ ARG IGPU_BUILD # Ensure apt-get won't prompt for selecting options ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && \ +RUN --mount=type=secret,id=apt_sources,target=/etc/apt/sources.list.d/nvidia-artifactory-ubuntu.list,required=false \ + apt-get update && \ apt-get install -y --no-install-recommends \ build-essential \ libarchive-dev \ @@ -177,7 +178,9 @@ RUN mkdir -p qa/custom_models/custom_sequence_int32/1 && \ # L0_lifecycle needs No-GPU build of identity backend. WORKDIR /workspace/tritonbuild/identity -RUN rm -rf install build && mkdir build && cd build && \ +RUN --mount=type=secret,id=gitconfig,target=/run/secrets/gitconfig,required=false \ + export GIT_CONFIG_GLOBAL=/run/secrets/gitconfig && \ + rm -rf install build && mkdir build && cd build && \ cmake -DTRITON_ENABLE_GPU=OFF \ -DCMAKE_INSTALL_PREFIX:PATH=/workspace/tritonbuild/identity/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ @@ -190,7 +193,9 @@ RUN rm -rf install build && mkdir build && cd build && \ # L0_backend_python test require triton_shm_monitor ARG TRITON_BOOST_URL="https://archives.boost.io/release/1.80.0/source/boost_1_80_0.tar.gz" WORKDIR /workspace/tritonbuild/python -RUN rm -rf install build && mkdir build && cd build && \ +RUN --mount=type=secret,id=gitconfig,target=/run/secrets/gitconfig,required=false \ + export GIT_CONFIG_GLOBAL=/run/secrets/gitconfig && \ + rm -rf install build && mkdir build && cd build && \ cmake -DCMAKE_INSTALL_PREFIX:PATH=/workspace/tritonbuild/python/install \ -DTRITON_REPO_ORGANIZATION:STRING=${TRITON_REPO_ORGANIZATION} \ -DTRITON_COMMON_REPO_TAG:STRING=${TRITON_COMMON_REPO_TAG} \ @@ -316,7 +321,8 @@ ARG TARGETPLATFORM ENV DEBIAN_FRONTEND=noninteractive # install platform specific packages -RUN if grep -qE '^VERSION_ID="(18\.04|20\.04|22\.04|24\.04)' /etc/os-release; then \ +RUN --mount=type=secret,id=apt_sources,target=/etc/apt/sources.list.d/nvidia-artifactory-ubuntu.list,required=false \ + if grep -qE '^VERSION_ID="(18\.04|20\.04|22\.04|24\.04)' /etc/os-release; then \ apt-get update && \ apt-get install -y --no-install-recommends \ libpng-dev && \ @@ -329,7 +335,8 @@ RUN if grep -qE '^VERSION_ID="(18\.04|20\.04|22\.04|24\.04)' /etc/os-release; th # CI/QA for memcheck requires valgrind # libarchive-dev is required by Python backend -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN --mount=type=secret,id=apt_sources,target=/etc/apt/sources.list.d/nvidia-artifactory-ubuntu.list,required=false \ + apt-get update && apt-get install -y --no-install-recommends \ curl \ gdb \ libarchive-dev \ diff --git a/Dockerfile.sdk b/Dockerfile.sdk index 6697aff946..f8b23ab067 100644 --- a/Dockerfile.sdk +++ b/Dockerfile.sdk @@ -125,7 +125,9 @@ COPY TRITON_VERSION . COPY ${TRITON_CLIENT_REPO_SUBDIR} client WORKDIR /workspace/client_build -RUN cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ +RUN --mount=type=secret,id=gitconfig,target=/run/secrets/gitconfig,required=false \ + export GIT_CONFIG_GLOBAL=/run/secrets/gitconfig && \ + cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_VERSION=`cat /workspace/TRITON_VERSION` \ -DTRITON_REPO_ORGANIZATION=${TRITON_REPO_ORGANIZATION} \ -DTRITON_COMMON_REPO_TAG=${TRITON_COMMON_REPO_TAG} \ @@ -138,11 +140,15 @@ RUN cmake -DCMAKE_INSTALL_PREFIX=/workspace/install \ -DTRITON_ENABLE_EXAMPLES=ON -DTRITON_ENABLE_TESTS=ON \ -DTRITON_ENABLE_GPU=${TRITON_ENABLE_GPU} /workspace/client RUN --mount=type=secret,id=maven_settings,target=/run/secrets/maven_settings,required=false \ + --mount=type=secret,id=gitconfig,target=/run/secrets/gitconfig,required=false \ + export GIT_CONFIG_GLOBAL=/run/secrets/gitconfig && \ if [ -f /run/secrets/maven_settings ]; then export MAVEN_ARGS="--settings /run/secrets/maven_settings"; fi && \ cmake --build . -v --parallel ${TRITON_CLIENT_BUILD_PARALLEL} --target cc-clients java-clients python-clients # Install Java API Bindings RUN --mount=type=secret,id=maven_settings,target=/run/secrets/maven_settings,required=false \ + --mount=type=secret,id=gitconfig,target=/run/secrets/gitconfig,required=false \ + export GIT_CONFIG_GLOBAL=/run/secrets/gitconfig && \ if [ -f /run/secrets/maven_settings ]; then export MAVEN_ARGS="--settings /run/secrets/maven_settings"; fi && \ if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ source /workspace/client/src/java-api-bindings/scripts/install_dependencies_and_build.sh \ @@ -167,7 +173,8 @@ ARG TRITON_CORE_REPO_TAG ARG TARGETPLATFORM ARG TRITON_ENABLE_GPU -RUN apt-get update && \ +RUN --mount=type=secret,id=apt_sources,target=/etc/apt/sources.list.d/nvidia-artifactory-ubuntu.list,required=false \ + apt-get update && \ apt-get install -y --no-install-recommends \ curl \ default-jdk \ @@ -257,7 +264,9 @@ RUN rm -f /usr/bin/python && \ # Install Model Analyzer ARG TRITON_MODEL_ANALYZER_REPO_TAG ARG TRITON_MODEL_ANALYZER_REPO="${TRITON_REPO_ORGANIZATION}/model_analyzer@${TRITON_MODEL_ANALYZER_REPO_TAG}" -RUN pip3 install "git+${TRITON_MODEL_ANALYZER_REPO}" +RUN --mount=type=secret,id=gitconfig,target=/run/secrets/gitconfig,required=false \ + export GIT_CONFIG_GLOBAL=/run/secrets/gitconfig && \ + pip3 install "git+${TRITON_MODEL_ANALYZER_REPO}" # Entrypoint Banner ENV NVIDIA_PRODUCT_NAME="Triton Server SDK" diff --git a/build.py b/build.py index 68ee00b91f..4a2fad94db 100755 --- a/build.py +++ b/build.py @@ -878,6 +878,122 @@ def install_dcgm_libraries(dcgm_version, target_machine): ) +# Keys that 'docker build --secret' accepts. A spec may also carry keys that +# only the Dockerfile side understands ('target', 'required', 'mode', ...); +# passing those to the build command is an error, so they are filtered out. +DOCKER_BUILD_SECRET_KEYS = ("id", "src", "source", "env", "type") + +# Id whose secret is mounted over the apt source list, and where it lands when +# the spec does not say. +APT_SOURCES_SECRET_ID = "apt_sources" +APT_SOURCES_DEFAULT_TARGET = "/etc/apt/sources.list.d/nvidia-artifactory-ubuntu.list" + +# Variable holding a git config, and where the build container writes it out. +# The container is handed the contents rather than the file because docker +# resolves a bind mount source on the daemon, which need not share a filesystem +# with this script, and silently mounts a directory when it finds nothing there. +GIT_CONFIG_CONTENT_ENV = "TRITON_GITCONFIG" +GIT_CONFIG_CONTAINER_PATH = "/tmp/gitconfig" + + +def parse_secret_spec(spec): + """Split a Docker secret spec into ordered (key, value) pairs. + + Docker does not require any particular field order, so callers look up + keys by name rather than by position. + """ + fields = [] + for field in spec.split(","): + key, _, value = field.partition("=") + key = key.strip() + if key: + fields.append((key, value.strip())) + return fields + + +def secret_spec_value(spec, name): + """Return the value of one field of a secret spec, or "" when unset.""" + for key, value in parse_secret_spec(spec): + if key == name: + return value + return "" + + +def secret_spec_id(spec): + """Return the 'id' of a Docker secret spec, or "" when it has none.""" + return secret_spec_value(spec, "id") + + +def declared_secret_specs(): + """Every --docker-build-secret spec, in the order it was given.""" + return [spec for spec in getattr(FLAGS, "docker_build_secret", None) or [] if spec] + + +def declared_secret_ids(): + """Ids of every secret supplied with --docker-build-secret.""" + return {i for i in (secret_spec_id(s) for s in declared_secret_specs()) if i} + + +def secret_build_args(): + """Return the '--secret' arguments to forward to 'docker build'. + + Only the keys the build command understands are forwarded. Keys that + belong to the Dockerfile mount are dropped here and applied where the + Dockerfile is generated instead. + """ + args = [] + for spec in declared_secret_specs(): + fields = [ + "{}={}".format(key, value) + for key, value in parse_secret_spec(spec) + if key in DOCKER_BUILD_SECRET_KEYS + ] + if fields: + args.append("--secret {}".format(",".join(fields))) + return args + + +def apt_sources_secret_mount(): + """Return the RUN mount flag that overlays the apt source list, or an + empty string when no such secret was requested. + + The mount point comes from the spec's 'target' when it has one, so + '--docker-build-secret id=apt_sources,src=,target=' controls + where the list lands. Without a 'target' it defaults to the NVIDIA + Artifactory source list. When the secret is absent this returns "" and the + generated Dockerfiles are byte-identical to a build without this feature. + """ + for spec in declared_secret_specs(): + if secret_spec_id(spec) != APT_SOURCES_SECRET_ID: + continue + target = secret_spec_value(spec, "target") or APT_SOURCES_DEFAULT_TARGET + return "--mount=type=secret,id={},target={},required=false ".format( + APT_SOURCES_SECRET_ID, target + ) + return "" + + +def mount_apt_sources_secret(df): + """Prefix every 'RUN apt-get update' in df with the apt source list mount. + + A secret mount is scoped to a single RUN instruction, so every apt step + that should resolve through the mirror needs its own. + + The first step of the build base is included even though it is the step + that installs ca-certificates. On a base image that already carries them + the step resolves through the mirror like any other. On one that does not, + apt reports the failed TLS handshake, still exits 0, and installs from the + distribution repositories, so the step behaves as it did before the mount + was added. + """ + mount = apt_sources_secret_mount() + if not mount: + return df + + marker = "RUN apt-get update" + return df.replace(marker, "RUN " + mount + "apt-get update") + + def create_dockerfile_buildbase_rhel(ddir, dockerfile_name, argmap): df = """ ARG TRITON_VERSION={} @@ -1131,6 +1247,8 @@ def create_dockerfile_buildbase(ddir, dockerfile_name, argmap): ENTRYPOINT [] """ + df = mount_apt_sources_secret(df) + with open(os.path.join(ddir, dockerfile_name), "w") as dfile: dfile.write(df) @@ -1259,6 +1377,8 @@ def create_dockerfile_linux( ldconfig """ + df = mount_apt_sources_secret(df) + with open(os.path.join(ddir, dockerfile_name), "w") as dfile: dfile.write(df) @@ -1683,6 +1803,8 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ baseargs += ["--cache-from={}".format(k) for k in cachefrommap] + baseargs += secret_build_args() + baseargs += ["."] docker_script.cwd(THIS_SCRIPT_DIR) @@ -1729,6 +1851,19 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ # or explicit --release-version). Dev / pre-release builds leave it # unset so build_wheel.py reads the in-tree TRITON_VERSION file and # takes the PEP 817 variant path. + # Authenticate the GitHub clones cmake_build performs in this container. + # It runs the build rather than an image build, so a build secret cannot + # reach it, and a bind mount cannot either: docker resolves the source + # path on the daemon, which need not share the filesystem this script + # runs on, and silently creates a directory when it does not find it. + # Hand the container the contents instead and let it write the file. + # + # Passed by name rather than as NAME=value, so the contents, which carry + # a credential, stay out of this script, which CI publishes as a build + # artifact. + if os.environ.get(GIT_CONFIG_CONTENT_ENV): + runargs += ["-e", GIT_CONFIG_CONTENT_ENV] + if "TRITON_RELEASE_VERSION" in os.environ: runargs += [ "-e", @@ -1745,7 +1880,23 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ runargs += ["tritonserver_buildbase"] - runargs += ["./cmake_build"] + # Write the git config out inside the container, where the contents + # arrived as an environment variable, and point git at it for the build + # only. Single quoted so the outer shell leaves the expansion to the + # container, whose environment is the one holding the value. + if os.environ.get(GIT_CONFIG_CONTENT_ENV): + runargs += [ + "bash", + "-c", + '\'printf "%s" "${}" > {} && export GIT_CONFIG_GLOBAL={} && ' + "./cmake_build'".format( + GIT_CONFIG_CONTENT_ENV, + GIT_CONFIG_CONTAINER_PATH, + GIT_CONFIG_CONTAINER_PATH, + ), + ] + else: + runargs += ["./cmake_build"] # Remove existing tritonserver_builder container... docker_script._file.write( @@ -1785,14 +1936,7 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ "docker", "build", ] - if secrets: - finalargs += [ - f"--secret id=req,src={requirements}", - "--secret id=VLLM_INDEX_URL", - "--secret id=PYTORCH_TRITON_URL", - "--secret id=NVPL_SLIM_URL", - f"--build-arg BUILD_PUBLIC_VLLM={build_public_vllm}", - ] + finalargs += secret_build_args() finalargs += [ "-t", "tritonserver", @@ -2623,15 +2767,25 @@ def enable_all(): help="This flag sets the Python version for RHEL platform of Triton Inference Server to be built. Default: the latest supported version.", ) parser.add_argument( - "--build-secret", + "--docker-build-secret", action="append", required=False, - nargs=2, - metavar=("key", "value"), - help="Add build secrets in the form of . These secrets are used during the build process for vllm. The secrets are passed to the Docker build step as `--secret id=`. The following keys are expected and their purposes are described below:\n\n" - " - 'req': A file containing a list of dependencies for pip (e.g., requirements.txt).\n" - " - 'build_public_vllm': A flag (default is 'true') indicating whether to build the public VLLM version.\n\n" - "Ensure that the required environment variables for these secrets are set before running the build.", + metavar="spec", + help="Pass a build secret to 'docker build' using Docker's own syntax. The spec is forwarded " + "unchanged, so every form 'docker build --secret' accepts is supported:\n\n" + " - 'id=,src=' read the secret from a file (source= is an accepted alias)\n" + " - 'id=,env=' read the secret from an environment variable\n" + " - 'id=' read the environment variable of the same name\n\n" + "May be repeated. The secret is available to a Dockerfile step that mounts it with " + "'RUN --mount=type=secret,id=', and never becomes part of an image layer.\n\n" + "A spec may additionally carry 'target='. Docker rejects that key on the command line " + "because it belongs to the Dockerfile mount, so it is stripped from the build command and " + "applied where the Dockerfile is generated.\n\n" + "The id 'apt_sources' carries extra meaning: it is also mounted for the duration of each apt step " + "in the generated Dockerfile and Dockerfile.buildbase, so package installs resolve through a " + "package mirror. It lands on 'target' when the spec sets one, and on " + "/etc/apt/sources.list.d/nvidia-artifactory-ubuntu.list otherwise. When the secret is omitted the " + "generated Dockerfiles are unchanged and apt uses the distribution repositories.", ) parser.add_argument( "--triton-wheels-dependencies-group", @@ -2666,8 +2820,8 @@ def enable_all(): FLAGS.override_backend_cmake_arg = [] if FLAGS.extra_backend_cmake_arg is None: FLAGS.extra_backend_cmake_arg = [] - if FLAGS.build_secret is None: - FLAGS.build_secret = [] + if FLAGS.docker_build_secret is None: + FLAGS.docker_build_secret = [] FLAGS.boost_url = os.getenv( "TRITON_BOOST_URL", @@ -2777,12 +2931,6 @@ def enable_all(): ) backends["python"] = backends["vllm"] - secrets = dict(getattr(FLAGS, "build_secret", [])) - if secrets: - requirements = secrets.get("req", "") - build_public_vllm = secrets.get("build_public_vllm", "true") - log('Build Arg for BUILD_PUBLIC_VLLM: "{}"'.format(build_public_vllm)) - # Initialize map of repo agents to build and repo-tag for each. repoagents = {} for be in FLAGS.repoagent: diff --git a/docs/customization_guide/build.md b/docs/customization_guide/build.md index b37f579339..0e1ca25908 100644 --- a/docs/customization_guide/build.md +++ b/docs/customization_guide/build.md @@ -180,6 +180,84 @@ you have a branch called "mybranch" in the repo that you want to use in the build, you would specify --backend=onnxruntime:mybranch. +#### Build Secrets + +Some builds need a credential, for example an apt source list naming an +authenticated package mirror. Pass these with --docker-build-secret, which +takes a Docker build secret spec and forwards it to `docker build --secret` +unchanged. Every form Docker accepts works and the flag may be repeated. + +```bash +$ ./build.py ... --docker-build-secret id=,src= --docker-build-secret id=,env= +``` + +A secret is mounted only for the duration of the build step that uses it and +never becomes part of an image layer. + +A spec may also carry target=``, which selects where the secret is +mounted. Docker rejects that key on the command line because it belongs to the +Dockerfile rather than the build command, so build.py removes it before +invoking docker and applies it when generating the Dockerfile. + +The id `apt_sources` has an additional meaning. Its secret is mounted for the +duration of every apt step in the generated Dockerfile and Dockerfile.buildbase, +so package installs resolve through the mirror the list names. It lands on +target when the spec sets one, and on +/etc/apt/sources.list.d/nvidia-artifactory-ubuntu.list otherwise. When the +secret is omitted the generated Dockerfiles are unchanged and apt uses the +distribution repositories. + +This includes the first step of Dockerfile.buildbase, which is also the step +that installs ca-certificates. On a base image that already carries them the +step resolves through the mirror like any other. On one that does not, apt +reports the failed TLS handshake, still exits 0, and installs from the +distribution repositories, so the step behaves as it did before the mount was +added. + +#### Authenticating Clones From GitHub + +Source from several other repos is fetched during the build, as described +above. Those clones are unauthenticated by default, which is subject to +GitHub's rate limits and cannot reach a private repo. Supply a git config that +rewrites the GitHub URL to an authenticated one, as the `gitconfig` build +secret. + +```bash +$ cat > /tmp/gitconfig <@github.com/"] + insteadOf = https://github.com/ +EOF +$ chmod 600 /tmp/gitconfig +$ ./build.py ... --docker-build-secret id=gitconfig,src=/tmp/gitconfig +``` + +The token has to appear in the file. git does not expand environment variables +inside `url..insteadOf`, so a config that refers to one is stored +literally and silently fails to authenticate. + +That covers the steps that clone while an image is being built. build.py mounts +the config on each of them and points git at it with GIT_CONFIG_GLOBAL rather +than installing it at the default path, so nothing outside those steps picks it +up. The config is never part of an image layer, and only its path reaches the +generated build scripts. + +cmake_build clones while the build runs inside the container rather than while +an image is built, so no build secret reaches it. Neither does a bind mount: +docker resolves the source path on the daemon, which need not share a filesystem +with build.py, and silently mounts a directory when it finds nothing there. Put +the same text in TRITON_GITCONFIG for that case. build.py forwards the variable +to the container by name and has it write the file there, so the contents stay +out of the generated build scripts. + +```bash +$ export TRITON_GITCONFIG="$(cat /tmp/gitconfig)" +$ ./build.py ... +``` + +Set both when a build needs authenticated clones throughout: the secret for the +image builds, the variable for the build itself. Omit either and the steps it +covers clone unauthenticated. + #### Experimental: Build Presets > **Experimental.** This feature is gated behind the