Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,26 @@ oci.pull(
],
tag = "21-jre",
)

# NVIDIA distroless java: the base for Java service images, matching the
# distroless_go / distroless_cc bases the Go and Rust services use and the base
# the pre-Bazel nvct-service Dockerfile used (25-jdk-v4.0.8). Publicly
# pullable, multi-arch. Pinned by digest like the other distroless bases.
# JDK 25 matches --java_language_version=25 above.
oci.pull(
name = "distroless_java",
digest = "sha256:cc9dbc2560e39f6e0a67cd85421370e46fd818b1660800b507b82819a79ea21f",
image = "nvcr.io/nvidia/distroless/java",
platforms = [
"linux/amd64",
"linux/arm64/v8",
],
)
use_repo(
oci,
"distroless_java",
"distroless_java_linux_amd64",
"distroless_java_linux_arm64_v8",
"temurin_jre",
"temurin_jre_linux_amd64",
"temurin_jre_linux_arm64_v8",
Expand Down
50 changes: 48 additions & 2 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions rules/oci/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@
"OCI image rules for packaging binaries into multi-arch containers."

load("//rules/oci/private:go.bzl", _go_oci_image = "go_oci_image")
load("//rules/oci/private:java.bzl", _java_oci_image = "java_oci_image")

go_oci_image = _go_oci_image
java_oci_image = _java_oci_image
9 changes: 8 additions & 1 deletion rules/oci/private/common.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ def create_oci_image(
entrypoint,
visibility,
registry = None,
tags = None):
tags = None,
env = None,
workdir = None):
"""Creates OCI image targets with platform transitions and tarball output.

Generates:
Expand All @@ -44,6 +46,9 @@ def create_oci_image(
- {name}_load: Local docker load target
- {name}.tar: Tarball filegroup
- {name}_push: Push to registry (if registry is set)

env and workdir are optional and default to leaving the base image's
values untouched, so existing callers are unaffected.
"""
all_tags = ["manual"] + (tags or [])

Expand All @@ -53,6 +58,8 @@ def create_oci_image(
base = base,
tars = tars + COMMON_LAYERS,
entrypoint = entrypoint,
env = env,
workdir = workdir,
visibility = ["//visibility:private"],
tags = all_tags,
)
Expand Down
161 changes: 161 additions & 0 deletions rules/oci/private/java.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.

"OCI image rules for Java applications."

load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files")
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
load("//rules/oci/private:common.bzl", "create_oci_image")

# NVIDIA distroless java, matching the distroless_go / distroless_cc bases the
# Go and Rust services already use. Publicly pullable, multi-arch, and the same
# base the pre-Bazel service Dockerfiles use. Keep the JDK major version in
# lockstep with --java_language_version in //.bazelrc: a runtime older than the
# emitted bytecode fails at container startup with UnsupportedClassVersionError,
# which no build-time check catches.
DEFAULT_JAVA_BASE = "@distroless_java"

# The distroless bases set ENTRYPOINT ["/usr/bin/shelless_ulimit"], a shim that
# raises the soft file-descriptor limit before exec'ing the real command.
# oci_image REPLACES the base entrypoint rather than appending, so omitting the
# shim here would silently drop it and leave the container on the default 1024
# soft limit. Same fix grpc-proxy applies for its Go image.
ULIMIT_SHIM = "/usr/bin/shelless_ulimit"

# Arch-stable java path. Do NOT build this from JAVA_HOME: the base sets it
# per-architecture (/usr/lib/jvm/temurin-25-jdk-amd64 vs -arm64), so a
# hardcoded JAVA_HOME path would break the arm64 half of the image index.
# /usr/bin/java is present on both arches.
JAVA_BIN = "/usr/bin/java"

def _java_oci_image_impl(name, visibility, jar, base, jar_path, java_bin, entrypoint, jvm_flags, env, workdir, registry, tags):
layer_name = name + "_layer"
files_name = name + "_files"

if not jar_path.startswith("/"):
fail("jar_path must be absolute, got: " + jar_path)
jar_dir, _, jar_name = jar_path.rpartition("/")

# Install the jar AT jar_path. pkg_tar alone preserves the source
# basename and only the entrypoint would name jar_path, so a jar not
# already called app.jar, or a jar_path pointing elsewhere, would produce
# an image whose entrypoint references a file that does not exist. pkg_files
# renames and relocates it so the layer and the entrypoint cannot disagree.
#
# mode 0644 is correct and is NOT the go_oci_image case: a jar is read by
# the JVM, never exec'd, so it needs no exec bit. The executable in the
# entrypoint is java, which comes from the base image.
pkg_files(
name = files_name,
srcs = [jar],
prefix = jar_dir,
renames = {jar: jar_name},
attributes = pkg_attributes(mode = "0644"),
visibility = ["//visibility:private"],
)

pkg_tar(
name = layer_name,
extension = "tar.gz",
srcs = [files_name],
visibility = ["//visibility:private"],
)

entry = entrypoint
if not entry:
entry = [ULIMIT_SHIM, java_bin] + list(jvm_flags) + ["-jar", jar_path]

create_oci_image(
name = name,
tars = [layer_name],
base = base,
entrypoint = entry,
env = env,
workdir = workdir,
visibility = visibility,
registry = registry,
tags = tags,
)

java_oci_image = macro(
doc = """Packages a Java application jar into a multi-arch OCI image.

Intended for a Spring Boot executable jar (see //rules/java:spring.bzl),
but works with any self-contained runnable jar. Produces the same target
set as go_oci_image: {name}, {name}_index (multi-arch, this is what you
push), {name}_load, {name}.tar, and {name}_push when registry is set.
""",
implementation = _java_oci_image_impl,
attrs = {
"jar": attr.label(
doc = "The runnable jar to package (e.g. a spring_boot_app target).",
mandatory = True,
allow_single_file = True,
configurable = False,
),
"base": attr.label(
doc = """Base OCI image. It must provide a Java launcher at the
absolute path given by java_bin (default /usr/bin/java) and the
shelless_ulimit shim, which the NVIDIA distroless bases do. A base
with Java only on PATH, or at a different absolute path, must set
java_bin accordingly.""",
default = DEFAULT_JAVA_BASE,
configurable = False,
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"java_bin": attr.string(
doc = """Absolute path to the Java launcher in the base image.
Absolute rather than PATH-resolved, and deliberately not derived
from JAVA_HOME, which the distroless bases set per architecture.""",
default = JAVA_BIN,
configurable = False,
),
"jar_path": attr.string(
doc = """Absolute in-image path the jar is installed at. The layer
and the entrypoint are both derived from this, so they cannot
disagree.""",
default = "/app.jar",
configurable = False,
),
"env": attr.string_dict(
doc = """Environment variables set in the image. Required for
runtime behavior the base image expects, for example ULIMIT_FLAG=1,
without which the shelless_ulimit shim runs but raises no limit.""",
configurable = False,
),
"workdir": attr.string(
doc = "Container working directory. Unset leaves the base's value.",
configurable = False,
),
"jvm_flags": attr.string_list(
doc = "JVM flags inserted before -jar. Ignored when entrypoint is set.",
configurable = False,
),
"entrypoint": attr.string_list(
doc = """Container entrypoint. Defaults to
[/usr/bin/shelless_ulimit, /usr/bin/java, {jvm_flags}..., -jar, {jar_path}].
If you override this, keep the shelless_ulimit shim first or the
container loses the raised file-descriptor limit.""",
configurable = False,
),
"registry": attr.string(
doc = "Registry to push to. If not set, push target is not created.",
configurable = False,
),
"tags": attr.string_list(
doc = "Tags for generated targets. 'manual' is always added.",
configurable = False,
),
},
)
40 changes: 40 additions & 0 deletions src/control-plane-services/cloud-tasks/nvct-service/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
load("//rules/java:defs.bzl", "nvct_library", "nvct_library_test")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//rules/java:spring.bzl", "spring_boot_app")
load("//rules/oci:defs.bzl", "java_oci_image")

package(default_visibility = ["//visibility:public"])

Expand Down Expand Up @@ -74,3 +76,41 @@ nvct_library_test(
],
timeout = "long",
)

# Release image. Push the _index label (multi-arch amd64 + arm64), never
# :nvct-service-oss-image, which is the single host-arch image.
#
# Runtime parity with the pre-Bazel nvct-service/Dockerfile, which is the
# contract this image must preserve:
# COPY app.jar /usr/share/app.jar
# ENV ULIMIT_FLAG=1
# ENV JDK_JAVA_OPTIONS="-XX:MaxRAMPercentage=40.0 ..."
# WORKDIR /home/app
# CMD ["java", "-jar", "/usr/share/app.jar"]
#
# ULIMIT_FLAG is load-bearing, not decoration: the base's shelless_ulimit shim
# reads it, so without it the shim runs and raises nothing. JDK_JAVA_OPTIONS
# carries the container heap sizing (MaxRAMPercentage), so dropping it silently
# changes JVM memory behavior in production.
java_oci_image(
name = "nvct-service-oss-image",
jar = ":app",
jar_path = "/usr/share/app.jar",
env = {
"ULIMIT_FLAG": "1",
"JDK_JAVA_OPTIONS": "-XX:MaxRAMPercentage=40.0 -XX:+EnableDynamicAgentLoading -Dreactor.netty.pool.maxIdleTime=30000 -Dreactor.netty.pool.maxConnections=500",
},
workdir = "/home/app",
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Runtime-contract guard for the image. Complements, and does not replace,
# :tests above (the Java suite).
#
# sh_test is loaded explicitly: cloud-tasks builds in the root module on Bazel
# 9, which no longer provides sh_test as a built-in global.
sh_test(
name = "image_contract_test",
srcs = ["image_contract_test.sh"],
args = ["$(location :nvct-service-oss-image.tar)"],
data = [":nvct-service-oss-image.tar"],
)
Loading
Loading