Skip to content

Commit

Permalink
feat(oci_python): demonstrate fine grained layering (#256)
Browse files Browse the repository at this point in the history
  • Loading branch information
thesayyn authored Sep 26, 2023
1 parent 453d1c4 commit 474217a
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 70 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
- 'next.js'
- 'oci_go_image'
- 'oci_java_image'
- 'oci_python_image'
- 'pnpm-workspaces'
- 'prisma'
- 'protobufjs'
Expand Down
15 changes: 10 additions & 5 deletions oci_python_image/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ python.toolchain(
)
use_repo(python, "python3_9")
use_repo(python, "python3_9_toolchains")
register_toolchains("@python3_9_toolchains//:all")

register_toolchains(
"@python3_9_toolchains//:all",
pip = use_extension("@rules_python//python:extensions.bzl", "pip")
pip.parse(
name = "pip",
requirements_lock = "//:requirements.txt",
)
use_repo(pip, "pip")

oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")

oci.pull(
name = "distroless_python",
# tag = "latest", # as of 30 August 2023
digest = "sha256:5148968d8ae02a0f6d12efaca7a16e711ab43a4695a285e22dbbae70d6048937",
platforms = ["linux/amd64", "linux/arm64/v8"],
platforms = [
"linux/amd64",
"linux/arm64/v8"
],
image = "gcr.io/distroless/python3",
)

use_repo(oci, "distroless_python")
22 changes: 13 additions & 9 deletions oci_python_image/hello_world/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
load("@rules_python//python:defs.bzl", "py_binary", "py_library")
load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_filegroup")
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_tarball")
load("//:py_image_layer.bzl", "py_image_layer")
load("@container_structure_test//:defs.bzl", "container_structure_test")
load("@pip//:requirements.bzl", "requirement")
load("//:py_image_layer.bzl", "py_image_layer")

py_library(
name = "hello_world",
name = "hello_world_lib",
srcs = [
"__init__.py",
"app.py",
Expand All @@ -15,24 +16,27 @@ py_library(
)

py_binary(
name = "hello_world_bin",
name = "hello_world",
srcs = ["__main__.py"],
imports = [".."],
main = "__main__.py",
visibility = ["//:__subpackages__"],
deps = [":hello_world"],
deps = [
":hello_world_lib",
requirement("cowsay"),
],
)

py_image_layer(
name = "hello_world_layer",
binary = ":hello_world_bin",
binary = ":hello_world",
root = "/opt",
)

oci_image(
name = "image",
base = "@distroless_python",
cmd = ["/opt/hello_world/hello_world_bin"],
cmd = ["/opt/hello_world/hello_world"],
tars = [":hello_world_layer"],
)

Expand All @@ -53,7 +57,7 @@ platform(
)

platform_transition_filegroup(
name = "transitioned_image",
name = "platform_image",
srcs = [":image"],
target_platform = select({
"@platforms//cpu:arm64": ":aarch64_linux",
Expand All @@ -65,12 +69,12 @@ platform_transition_filegroup(
# $ docker run --rm gcr.io/oci_python_hello_world:latest
oci_tarball(
name = "tarball",
image = ":image",
image = ":platform_image",
repo_tags = ["gcr.io/oci_python_hello_world:latest"],
)

container_structure_test(
name = "test",
configs = ["test.yaml"],
image = ":image",
image = ":platform_image",
)
4 changes: 2 additions & 2 deletions oci_python_image/hello_world/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from hello_world.app import Helloworld
from hello_world.app import Cow

if __name__ == "__main__":
app = Helloworld("John")
app = Cow("John")
app.say_hello()
6 changes: 4 additions & 2 deletions oci_python_image/hello_world/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class Helloworld:
import cowsay

class Cow:
def __init__(self, name):
self.name = name

def say_hello(self):
print(f"Hello {self.name}!")
cowsay.cow("hello py_image_layer!")
7 changes: 4 additions & 3 deletions oci_python_image/hello_world/test.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# See https://github.com/GoogleContainerTools/container-structure-test#command-tests
schemaVersion: 2.0.0
metadataTest:
entrypoint: ['/opt/hello_world/hello_world_bin']
cmd: ['/opt/hello_world/hello_world']
commandTests:
- name: run
command: /opt/hello_world/hello_world_bin
expectedOutput: ['Hello John!']
command: python3
args: [/opt/hello_world/hello_world]
expectedOutput: ['| hello py_image_layer! |']
41 changes: 39 additions & 2 deletions oci_python_image/py_image_layer.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
load("//workaround_rules_pkg_153:runfiles.bzl", "runfiles")
load("@rules_pkg//:pkg.bzl", "pkg_tar")

# this is a magic glob that will *only* external repositories that has *python* string in it.
# example this will match
# `opt/hello_world/hello_world_bin.runfiles/rules_python~0.21.0~python~python3_9_aarch64-unknown-linux-gnu/bin/python3`
# while ignoring
# `opt/hello_world/hello_world_bin.runfiles/_main/python_app`
PY_INTERPRETER_PATH_GLOB = "**/*.runfiles/*python*-*/**"

# this is a magic glob that will *only* external pip like repositories that has *site-packages* string in it.
SITE_PACKAGES_GLOB = "**/*.runfiles/*/site-packages/*/**"

def py_image_layer(name, binary, root = None, **kwargs):
"""Creates a tar file to add to a python image, output at `:<name>/app.tar`.
Expand All @@ -25,6 +35,7 @@ def py_image_layer(name, binary, root = None, **kwargs):

common_kwargs = {
"tags": kwargs.pop("tags", None),
"testonly": kwargs.pop("testonly", None),
"visibility": kwargs.pop("visibility", None),
}

Expand All @@ -36,13 +47,25 @@ def py_image_layer(name, binary, root = None, **kwargs):

pkg_tar_kwargs = dict(
kwargs,
# Be careful with this option. Leave it as is if you don't know what you are doing
strip_prefix = kwargs.pop("strip_prefix", "."),
**common_kwargs
)

runfiles(
name = "%s/app/runfiles" % name,
include = "**",
excludes = [PY_INTERPRETER_PATH_GLOB, SITE_PACKAGES_GLOB],
**runfiles_kwargs
)

runfiles(
name = "%s/interpreter/runfiles" % name,
include = PY_INTERPRETER_PATH_GLOB,
**runfiles_kwargs
)

runfiles(
name = "%s/site_packages/runfiles" % name,
include = SITE_PACKAGES_GLOB,
**runfiles_kwargs
)

Expand All @@ -52,9 +75,23 @@ def py_image_layer(name, binary, root = None, **kwargs):
**pkg_tar_kwargs
)

pkg_tar(
name = "%s/interpreter" % name,
srcs = ["%s/interpreter/runfiles" % name],
**pkg_tar_kwargs
)

pkg_tar(
name = "%s/site_packages" % name,
srcs = ["%s/site_packages/runfiles" % name],
**pkg_tar_kwargs
)

native.filegroup(
name = name,
srcs = [
"%s/interpreter" % name,
"%s/site_packages" % name,
"%s/app" % name,
],
**common_kwargs
Expand Down
1 change: 1 addition & 0 deletions oci_python_image/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cowsay
3 changes: 3 additions & 0 deletions oci_python_image/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
#
# bazel run //:requirements.update
#
cowsay==6.1 \
--hash=sha256:274b1e6fc1b966d53976333eb90ac94cb07a450a700b455af9fbdf882244b30a
# via -r ./requirements.in
92 changes: 45 additions & 47 deletions oci_python_image/workaround_rules_pkg_153/runfiles.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -6,81 +6,79 @@ See https://github.com/bazelbuild/rules_pkg/issues/153

load("@rules_pkg//:providers.bzl", "PackageFilegroupInfo", "PackageFilesInfo", "PackageSymlinkInfo")
load("@aspect_bazel_lib//lib:paths.bzl", "to_manifest_path")
load("@aspect_bazel_lib//lib:glob_match.bzl", "glob_match")

def _runfile_path(ctx, file, runfiles_dir):
return "/".join([runfiles_dir, to_manifest_path(ctx, file)])

def _should_include(destination, include, exclude):
included = include in destination or include == ""
excluded = exclude in destination and exclude != ""
return included and not excluded
def _glob(path, include, excludes = []):
if len(excludes):
for exclude in excludes:
if glob_match(exclude, path):
return False
return glob_match(include, path)

def _runfiles_impl(ctx):
default = ctx.attr.binary[DefaultInfo]
def _calculate_runfiles_dir(root, default_info):
manifest = default_info.files_to_run.runfiles_manifest
return "/".join([root, manifest.short_path.replace(manifest.basename, "")[:-1]])

executable = default.files_to_run.executable
def _runfiles_impl(ctx):
# setup
default_info = ctx.attr.binary[DefaultInfo]
executable = default_info.files_to_run.executable
executable_path = "/".join([ctx.attr.root, executable.short_path])
runfiles_dir = _calculate_runfiles_dir(ctx.attr.root, default_info)

file_map = {}

if _should_include(executable_path, ctx.attr.include, ctx.attr.exclude):
file_map[executable_path] = executable
manifest = {}

manifest = default.files_to_run.runfiles_manifest
runfiles_dir = "/".join([ctx.attr.root, manifest.short_path.replace(manifest.basename, "")[:-1]])
if _glob(executable_path, ctx.attr.include, ctx.attr.excludes):
manifest[executable_path] = executable

files = depset(transitive = [default.files, default.default_runfiles.files])

for file in files.to_list():
for file in depset(transitive = [default_info.files, default_info.default_runfiles.files]).to_list():
destination = _runfile_path(ctx, file, runfiles_dir)
if _should_include(destination, ctx.attr.include, ctx.attr.exclude):
file_map[destination] = file

# executable should not go into runfiles directory so we add it to files here.
files = depset([executable], transitive = [files])
if _glob(destination, ctx.attr.include, ctx.attr.excludes):
manifest[destination] = file

symlinks = []

# NOTE: symlinks is different than root_symlinks. See: https://bazel.build/rules/rules#runfiles_symlinks for distinction between
# root_symlinks and symlinks and why they have to be handled differently.
for symlink in default.data_runfiles.symlinks.to_list():
for symlink in default_info.data_runfiles.symlinks.to_list():
destination = "/".join([runfiles_dir, ctx.workspace_name, symlink.path])
if not _should_include(destination, ctx.attr.include, ctx.attr.exclude):
continue
if hasattr(file_map, destination):
file_map.pop(destination)
info = PackageSymlinkInfo(
target = "/%s" % _runfile_path(ctx, symlink.target_file, runfiles_dir),
destination = destination,
attributes = {"mode": "0777"},
)
symlinks.append([info, symlink.target_file.owner])

for symlink in default.data_runfiles.root_symlinks.to_list():
if _glob(destination, ctx.attr.include, ctx.attr.excludes):
if hasattr(manifest, destination):
manifest.pop(destination)
info = PackageSymlinkInfo(
target = "/%s" % _runfile_path(ctx, symlink.target_file, runfiles_dir),
destination = destination,
attributes = {"mode": "0777"},
)
symlinks.append([info, symlink.target_file.owner])

for symlink in default_info.data_runfiles.root_symlinks.to_list():
destination = "/".join([runfiles_dir, symlink.path])
if not _should_include(destination, ctx.attr.include, ctx.attr.exclude):
continue
if hasattr(file_map, destination):
file_map.pop(destination)
info = PackageSymlinkInfo(
target = "/%s" % _runfile_path(ctx, symlink.target_file, runfiles_dir),
destination = destination,
attributes = {"mode": "0777"},
)
symlinks.append([info, symlink.target_file.owner])
if _glob(destination, ctx.attr.include, ctx.attr.excludes):
if hasattr(manifest, destination):
manifest.pop(destination)
info = PackageSymlinkInfo(
target = "/%s" % _runfile_path(ctx, symlink.target_file, runfiles_dir),
destination = destination,
attributes = {"mode": "0777"},
)
symlinks.append([info, symlink.target_file.owner])

return [
PackageFilegroupInfo(
pkg_dirs = [],
pkg_files = [
[PackageFilesInfo(
dest_src_map = file_map,
dest_src_map = manifest,
attributes = {},
), ctx.label],
],
pkg_symlinks = symlinks,
),
DefaultInfo(files = files),
DefaultInfo(files = depset(direct = manifest.values())),
]

runfiles = rule(
Expand All @@ -92,6 +90,6 @@ runfiles = rule(
),
"root": attr.string(),
"include": attr.string(),
"exclude": attr.string(),
"excludes": attr.string_list(),
},
)

0 comments on commit 474217a

Please sign in to comment.