Skip to content

Commit

Permalink
feat: implement oci_image_index (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
thesayyn authored Dec 20, 2022
1 parent f29c0e6 commit 835d197
Show file tree
Hide file tree
Showing 11 changed files with 272 additions and 1 deletion.
5 changes: 5 additions & 0 deletions docs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ stardoc_with_diff_test(
bzl_library_target = "//oci/private:tarball",
)

stardoc_with_diff_test(
name = "image_index",
bzl_library_target = "//oci/private:image_index",
)

update_docs(name = "update")
46 changes: 46 additions & 0 deletions docs/image_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!-- Generated with Stardoc: http://skydoc.bazel.build -->

Implementation details for oci_image_index rule

<a id="#oci_image_index"></a>

## oci_image_index

<pre>
oci_image_index(<a href="#oci_image_index-name">name</a>, <a href="#oci_image_index-images">images</a>)
</pre>

Build a multi-architecture OCI compatible container image.

It takes number of `oci_image`s to create a fat multi-architecture image.

Requires `wc` and `shasum` to be installed on the execution machine.

```starlark
oci_image(
name = "app_linux_amd64"
)

oci_image(
name = "app_linux_arm64"
)

oci_image_index(
name = "app",
images = [
":app_linux_amd64",
":app_linux_arm64"
]
)
```


**ATTRIBUTES**


| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="oci_image_index-name"></a>name | A unique name for this target. | <a href="https://bazel.build/docs/build-ref.html#name">Name</a> | required | |
| <a id="oci_image_index-images"></a>images | List of labels to oci_image targets. | <a href="https://bazel.build/docs/build-ref.html#labels">List of labels</a> | required | |


4 changes: 4 additions & 0 deletions example/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ genrule(
},
no_match_error = "Please build on a arm64 or amd64 host",
),
message = select({
"@platforms//cpu:arm64": "Pulling base image for linux/arm64",
"@platforms//cpu:x86_64": "Pulling base image for linux/amd64",
}),
output_to_bindir = True,
toolchains = [
"@oci_crane_toolchains//:resolved_toolchain",
Expand Down
37 changes: 37 additions & 0 deletions example/multi_arch/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
load("//oci:defs.bzl", "oci_image", "oci_image_index")
load("@rules_pkg//:pkg.bzl", "pkg_tar")
load(":transition.bzl", "multi_arch")

pkg_tar(
name = "app",
srcs = ["test.bash"],
)

oci_image(
name = "image",
architecture = select({
"@platforms//cpu:arm64": "arm64",
"@platforms//cpu:x86_64": "amd64",
}),
base = "//example:base",
cmd = ["test.bash"],
entrypoint = ["bash"],
os = "linux",
tars = ["app.tar"],
)

multi_arch(
name = "images",
image = ":image",
platforms = [
"//example:linux_arm64",
"//example:linux_amd64",
],
)

oci_image_index(
name = "index",
images = [
":images",
],
)
1 change: 1 addition & 0 deletions example/multi_arch/test.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
echo "This is rules_oci!"
27 changes: 27 additions & 0 deletions example/multi_arch/transition.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"a rule transitioning an oci_image to multiple platforms"

def _multiarch_transition(settings, attr):
return [
{"//command_line_option:platforms": str(platform)}
for platform in attr.platforms
]

multiarch_transition = transition(
implementation = _multiarch_transition,
inputs = [],
outputs = ["//command_line_option:platforms"],
)

def _impl(ctx):
return DefaultInfo(files = depset(ctx.files.image))

multi_arch = rule(
implementation = _impl,
attrs = {
"image": attr.label(cfg = multiarch_transition),
"platforms": attr.label_list(),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
)
2 changes: 2 additions & 0 deletions oci/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

load("//oci/private:tarball.bzl", _oci_tarball = "oci_tarball")
load("//oci/private:image.bzl", _oci_image = "oci_image")
load("//oci/private:image_index.bzl", _oci_image_index = "oci_image_index")
load("//oci/private:structure_test.bzl", _structure_test = "structure_test")

oci_tarball = _oci_tarball
oci_image = _oci_image
oci_image_index = _oci_image_index
structure_test = _structure_test
10 changes: 10 additions & 0 deletions oci/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ exports_files(

exports_files([
"image.sh.tpl",
"image_index.sh.tpl",
"tarball.sh.tpl",
])

Expand Down Expand Up @@ -44,6 +45,15 @@ bzl_library(
],
)

bzl_library(
name = "image_index",
srcs = ["image_index.bzl"],
visibility = [
"//docs:__pkg__",
"//oci:__subpackages__",
],
)

bzl_library(
name = "versions",
srcs = ["versions.bzl"],
Expand Down
79 changes: 79 additions & 0 deletions oci/private/image_index.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"Implementation details for oci_image_index rule"

_DOC = """Build a multi-architecture OCI compatible container image.
It takes number of `oci_image`s to create a fat multi-architecture image.
Requires `wc` and `shasum` to be installed on the execution machine.
```starlark
oci_image(
name = "app_linux_amd64"
)
oci_image(
name = "app_linux_arm64"
)
oci_image_index(
name = "app",
images = [
":app_linux_amd64",
":app_linux_arm64"
]
)
```
"""

_attrs = {
"images": attr.label_list(mandatory = True, doc = "List of labels to oci_image targets."),
"_image_index_sh_tpl": attr.label(default = "image_index.sh.tpl", allow_single_file = True),
}

def _expand_image_to_args(image, expander):
args = [
"--image={}".format(image.path),
]
for file in expander.expand(image):
if file.path.find("blobs") != -1:
args.append("--blob={}".format(file.tree_relative_path))
return args

def _oci_image_index_impl(ctx):
yq = ctx.toolchains["@aspect_bazel_lib//lib:yq_toolchain_type"]

launcher = ctx.actions.declare_file("image_index_{}.sh".format(ctx.label.name))
ctx.actions.expand_template(
template = ctx.file._image_index_sh_tpl,
output = launcher,
is_executable = True,
substitutions = {
"{{yq_path}}": yq.yqinfo.bin.path,
},
)

output = ctx.actions.declare_directory(ctx.label.name)

args = ctx.actions.args()
args.add(output.path, format = "--output=%s")
args.add_all(ctx.files.images, map_each = _expand_image_to_args, expand_directories = False)

ctx.actions.run(
inputs = ctx.files.images,
arguments = [args],
outputs = [output],
executable = launcher,
tools = [yq.yqinfo.bin],
progress_message = "OCI Index %{label}",
)

return DefaultInfo(files = depset([output]))

oci_image_index = rule(
implementation = _oci_image_index_impl,
attrs = _attrs,
doc = _DOC,
toolchains = [
"@aspect_bazel_lib//lib:yq_toolchain_type",
],
)
61 changes: 61 additions & 0 deletions oci/private/image_index.sh.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -o pipefail -o errexit -o nounset

readonly YQ="{{yq_path}}"


function add_image() {
local image_path="$1"
local output_path="$2"

local manifests=$("${YQ}" eval '.manifests[]' "${image_path}/index.json")

for manifest in "${manifests}"; do
local manifest_blob_path=$("${YQ}" '.digest | sub(":"; "/")' <<< ${manifest})
local config_blob_path=$("${YQ}" '.config.digest | sub(":"; "/")' "${image_path}/blobs/${manifest_blob_path}")

local platform=$("${YQ}" --output-format=json '{"os": .os, "architecture": .architecture, "variant": .variant, "os.version": .["os.version"], "os.features": .["os.features"]} | with_entries(select( .value != null ))' "${image_path}/blobs/${config_blob_path}")

platform="${platform}" \
manifest="${manifest}" \
"${YQ}" --inplace --output-format=json '.manifests += [env(manifest) + {"platform": env(platform)}]' "${output_path}/manifest_list.json"
done
}

function copy_blob() {
local image_path="$1"
local output_path="$2"
local blob_image_relative_path="$3"
local dest_path="${output_path}/${blob_image_relative_path}"
mkdir -p "$(dirname "${dest_path}")"
cat "${image_path}/${blob_image_relative_path}" > "${dest_path}"
}

function create_oci_layout() {
local path="$1"
mkdir -p "${path}"

echo '{"imageLayoutVersion": "1.0.0"}' > "${path}/oci-layout"
echo '{"schemaVersion": 2, "manifests": []}' > "${path}/index.json"
echo '{"schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests": []}' > "${path}/manifest_list.json"
}

CURRENT_IMAGE=""
OUTPUT=""

for ARG in "$@"; do
case "$ARG" in
(--output=*) OUTPUT="${ARG#--output=}"; create_oci_layout "$OUTPUT" ;;
(--image=*) CURRENT_IMAGE="${ARG#--image=}"; add_image "$CURRENT_IMAGE" "$OUTPUT" ;;
(--blob=*) copy_blob "${CURRENT_IMAGE}" "$OUTPUT" "${ARG#--blob=}" ;;
(*) echo "Unknown argument ${ARG}"; exit 1;;
esac
done


export checksum=$(shasum -a 256 "${OUTPUT}/manifest_list.json" | cut -f 1 -d " ")
export size=$(wc -c < "${OUTPUT}/manifest_list.json")

"${YQ}" --inplace --output-format=json '.manifests += [{"mediaType": "application/vnd.oci.image.index.v1+json", "size": env(size), "digest": "sha256:" + env(checksum)}]' "$OUTPUT/index.json"

mv "${OUTPUT}/manifest_list.json" "$OUTPUT/blobs/sha256/${checksum}"
1 change: 0 additions & 1 deletion oci/private/toolchains_repo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ TOOLCHAIN_TMPL = """\
toolchain(
name = "{platform}_toolchain",
exec_compatible_with = {compatible_with},
target_compatible_with = {compatible_with},
toolchain = "{toolchain}",
toolchain_type = "{toolchain_type}",
)
Expand Down

0 comments on commit 835d197

Please sign in to comment.