Skip to content
Draft
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
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module(
bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "aspect_bazel_lib", version = "2.19.4")
bazel_dep(name = "package_metadata", version = "0.0.10")

bazel_dep(name = "rules_pkg", version = "1.1.0", dev_dependency = True)
bazel_dep(name = "rules_oci", version = "2.2.6", dev_dependency = True)
Expand Down
9 changes: 8 additions & 1 deletion apko/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Overriding the default is only permitted in the root module.
apko_translate_lock = tag_class(attrs = {
"name": attr.string(mandatory = True),
"lock": attr.label(mandatory = True),
"vendors": attr.string_dict(
default = {},
doc = """Extra ``host -> PURL vendor`` entries for the PURL namespace
(e.g. ``{"mirror.corp.example": "corp"}``). Layered on top of the built-in
defaults. Unknown hosts fall back to ``alpine``.""",
),
})

def _apko_extension_impl(module_ctx):
Expand Down Expand Up @@ -68,9 +74,10 @@ def _apko_extension_impl(module_ctx):
control_checksum = package["control"]["checksum"],
data_range = package["data"]["range"],
data_checksum = package["data"]["checksum"],
vendors = lock.vendors,
)

translate_apko_lock(name = lock.name, target_name = lock.name, lock = lock.lock)
translate_apko_lock(name = lock.name, target_name = lock.name, lock = lock.lock, vendors = lock.vendors)

if mod.is_root:
deps = root_direct_dev_deps if module_ctx.is_dev_dependency(lock) else root_direct_deps
Expand Down
32 changes: 31 additions & 1 deletion apko/private/apk.bzl
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
"Repository rules for importing remote apk packages"

load("@bazel_skylib//lib:versions.bzl", "versions")
load("@package_metadata//purl:purl.bzl", "purl")
load(":util.bzl", "util")

APK_IMPORT_TMPL = """\
# Generated by apk_import. DO NOT EDIT
load("@package_metadata//rules:package_metadata.bzl", "package_metadata")

package_metadata(
name = "package_metadata",
purl = "{purl}",
visibility = ["//visibility:public"],
)

filegroup(
name = "all",
srcs = glob(
Expand Down Expand Up @@ -127,7 +136,24 @@ def _apk_import_impl(rctx):
control = control_output,
data = data_output,
)
rctx.file("BUILD.bazel", APK_IMPORT_TMPL)

# PURL spec: https://github.com/package-url/purl-spec/blob/main/types-doc/apk-definition.md
purl_string = (
purl.builder()
.type("apk")
.namespace(util.apk_namespace(rctx.attr.url, rctx.attr.vendors))
.name(rctx.attr.package_name)
.version(rctx.attr.version)
.add_qualifier("arch", rctx.attr.architecture)
.add_qualifier("checksum", "sha256:" + data_sha256)
.add_qualifier("repository_url", util.repo_url(rctx.attr.url, rctx.attr.architecture))
.build()
)
rctx.file("BUILD.bazel", APK_IMPORT_TMPL.format(purl = purl_string))
rctx.file(
"REPO.bazel",
"repo(default_package_metadata = [\"//:package_metadata\"])\n",
)

apk_import = repository_rule(
implementation = _apk_import_impl,
Expand All @@ -142,6 +168,10 @@ apk_import = repository_rule(
"control_checksum": attr.string(mandatory = True),
"data_range": attr.string(mandatory = True),
"data_checksum": attr.string(mandatory = True),
"vendors": attr.string_dict(
default = {},
doc = "Additional host -> PURL vendor namespace mappings, overlaid on top of util.DEFAULT_APK_VENDORS.",
),
},
)

Expand Down
27 changes: 27 additions & 0 deletions apko/private/util.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,38 @@ def _concatenate_gzip_segments(rctx, output, signature, control, data):
if r.return_code != 0:
fail("""concatenate_gzip_segments failed.\nstderr: {}\nstdout: {}""".format(r.stdout, r.stderr))

# Host -> PURL apk namespace. The PURL apk type spec requires a vendor
# namespace (https://github.com/package-url/purl-spec/blob/main/types-doc/apk-definition.md).
# Unknown hosts fall back to "alpine" since apk is Alpine's package format.
# Users can extend this map via the `vendors` attr on `apk.translate_lock(...)`.
DEFAULT_APK_VENDORS = {
"packages.wolfi.dev": "wolfi",
"packages.cgr.dev": "chainguard",
"dl-cdn.alpinelinux.org": "alpine",
}

def _apk_namespace(url, overrides = None):
"""Derive the PURL apk namespace (vendor) from a package download URL.

Args:
url: the package download URL (e.g. ``https://packages.wolfi.dev/os/...``).
overrides: optional ``dict[str, str]`` of host → vendor pairs that is
layered on top of ``DEFAULT_APK_VENDORS``. User entries win on
collision.
"""
after_scheme = url.split("://", 1)[-1]
host = after_scheme.split("/", 1)[0]
vendors = dict(DEFAULT_APK_VENDORS)
if overrides:
vendors.update(overrides)
return vendors.get(host, "alpine")

util = struct(
concatenate_gzip_segments = _concatenate_gzip_segments,
normalize_sri = _normalize_sri,
parse_lock = _parse_lock,
sanitize_string = _sanitize_string,
repo_url = _repo_url,
url_escape = _url_escape,
apk_namespace = _apk_namespace,
)
3 changes: 3 additions & 0 deletions apko/tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
load(":util_test.bzl", "util_test_suite")
load(":versions_test.bzl", "versions_test_suite")

versions_test_suite(name = "versions_test")

util_test_suite(name = "util_test")
66 changes: 66 additions & 0 deletions apko/tests/util_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Unit tests for `apko/private/util.bzl` helpers."""

load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
load("//apko/private:util.bzl", "util")

def _apk_namespace_known_hosts_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(
env,
"wolfi",
util.apk_namespace("https://packages.wolfi.dev/os/x86_64/foo-1.0.apk"),
)
asserts.equals(
env,
"chainguard",
util.apk_namespace("https://packages.cgr.dev/extras/x86_64/bar-1.0.apk"),
)
asserts.equals(
env,
"alpine",
util.apk_namespace("https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/musl-1.2.5-r11.apk"),
)
return unittest.end(env)

def _apk_namespace_unknown_host_falls_back_to_alpine_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(
env,
"alpine",
util.apk_namespace("https://mirror.example.test/v3.21/main/x86_64/foo-1.0.apk"),
)
return unittest.end(env)

def _apk_namespace_override_wins_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(
env,
"corp",
util.apk_namespace(
"https://mirror.corp.example/v3.21/main/x86_64/foo-1.0.apk",
{"mirror.corp.example": "corp"},
),
)

# Override on a known host wins over the built-in default.
asserts.equals(
env,
"custom-wolfi",
util.apk_namespace(
"https://packages.wolfi.dev/os/x86_64/foo-1.0.apk",
{"packages.wolfi.dev": "custom-wolfi"},
),
)
return unittest.end(env)

_apk_namespace_known_hosts_test = unittest.make(_apk_namespace_known_hosts_test_impl)
_apk_namespace_unknown_host_falls_back_to_alpine_test = unittest.make(_apk_namespace_unknown_host_falls_back_to_alpine_test_impl)
_apk_namespace_override_wins_test = unittest.make(_apk_namespace_override_wins_test_impl)

def util_test_suite(name):
unittest.suite(
name,
_apk_namespace_known_hosts_test,
_apk_namespace_unknown_host_falls_back_to_alpine_test,
_apk_namespace_override_wins_test,
)
6 changes: 6 additions & 0 deletions apko/translate_lock.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ APK_IMPORT_TMPL = """\
control_checksum = "{control_checksum}",
data_range = "{data_range}",
data_checksum = "{data_checksum}",
vendors = {vendors},
)
"""

Expand Down Expand Up @@ -100,6 +101,7 @@ def _translate_apko_lock_impl(rctx):
control_checksum = package["control"]["checksum"],
data_range = package["data"]["range"],
data_checksum = package["data"]["checksum"],
vendors = repr(rctx.attr.vendors),
))

for repository in lock_file["contents"]["repositories"]:
Expand All @@ -126,6 +128,10 @@ translate_apko_lock = repository_rule(
attrs = {
"lock": attr.label(doc = "label to the `apko.lock.json` file.", mandatory = True),
"target_name": attr.string(doc = "internal. do not use!"),
"vendors": attr.string_dict(
default = {},
doc = "host -> PURL vendor namespace overrides forwarded to each generated apk_import.",
),
},
doc = _DOC,
)