diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5026778 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea +/bazel-* diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 266b12d..6ce3176 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,64 @@ # rules_container_rpm -bazel rules to install and manage rpms inside of containers + +Bazel rules to install and manage rpms inside of containers. + +These rules can be used to install RPM packages into a cointainer and update its included RPM database without the need to run the container. +This allows building small and reproducible images with RPMs. Because the rpm database inside the container is also maintained, it can later be queried by any rpm binary to check what packages are installed. + +## Load it into your WORKSPACE + +``` +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "io_bazel_rules_container_rpm", + sha256 = "XYZ", + strip_prefix = "rules_container_rpm-0.0.1", + urls = ["https://github.com/rmohr/rules_container_rpm/archive/v0.0.1.tar.gz"], +) + +http_file( + name = "glibc", + url = "https://dl.fedoraproject.org/pub/fedora/linux/releases/28/Everything/x86_64/os/Packages/g/glibc-2.27-8.fc28.x86_64.rpm", + sha256 = "573ceb6ad74b919b06bddd7684a29ef75bc9f3741e067fac1414e05c0087d0b6" +) +``` + +## Use it in your BUILD file + + +``` +load( + "@io_bazel_rules_docker//container:container.bzl", + "container_image", +) + +load( + "@io_bazel_rules_container_rpm//rpm:rpm.bzl", + "rpm_image", +) + +container_image( + name = "files_base", + files = ["foo"], + mode = "0o644", +) + +rpm_image( + name = "allinone", + base = ":files_base", + rpms = ["@glibc//file", "@ca_certificates//file"], +) + +rpm_image( + name = "image2", + base = ":image1", + rpms = ["@ca_certificates//file"], +) + +rpm_image( + name = "image1", + base = ":files_base", + rpms = ["@glibc//file"], +) +``` diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..1f766f5 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,27 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "io_bazel_rules_docker", + sha256 = "29d109605e0d6f9c892584f07275b8c9260803bf0c6fcb7de2623b2bedc910bd", + strip_prefix = "rules_docker-0.5.1", + urls = ["https://github.com/bazelbuild/rules_docker/archive/v0.5.1.tar.gz"], +) + +load( + "@io_bazel_rules_docker//container:container.bzl", + container_repositories = "repositories", +) + +container_repositories() + +http_file( + name = "glibc", + url = "https://dl.fedoraproject.org/pub/fedora/linux/releases/28/Everything/x86_64/os/Packages/g/glibc-2.27-8.fc28.x86_64.rpm", + sha256 = "573ceb6ad74b919b06bddd7684a29ef75bc9f3741e067fac1414e05c0087d0b6" +) + +http_file( + name = "ca_certificates", + url = "https://dl.fedoraproject.org/pub/fedora/linux/releases/28/Everything/x86_64/os/Packages/c/ca-certificates-2018.2.22-3.fc28.noarch.rpm", + sha256 = "dfc3d2bf605fbea7db7f018af53fe0563628f788a40cb1e7f84434606b7b6a12" +) diff --git a/rpm/BUILD b/rpm/BUILD new file mode 100644 index 0000000..7adb472 --- /dev/null +++ b/rpm/BUILD @@ -0,0 +1,26 @@ +# Copyright 2017 The Bazel Authors. All rights reserved. +# +# 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. +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # Apache 2.0 + +py_binary( + name = "install_rpms", + srcs = [":install_rpms.py"], + srcs_version = "PY2AND3", + visibility = ["//visibility:public"], + deps = [ + "@bazel_tools//tools/build_defs/pkg:archive", + ], +) diff --git a/rpm/install_rpms.py b/rpm/install_rpms.py new file mode 100644 index 0000000..f787899 --- /dev/null +++ b/rpm/install_rpms.py @@ -0,0 +1,53 @@ +import subprocess +import tempfile +import shutil +import tarfile +import argparse +from os import path + + +parser = argparse.ArgumentParser( + description='Install RPMs and update the RPM database inside the docker image') + +parser.add_argument('--uncompressed_layer', action='append', required=True, + help='The output file, mandatory') + +parser.add_argument('--rpm', action='append', required=True, + help=('rpms to add to the database')) + +parser.add_argument('--output', action='store', required=True, + help=('target archive')) + +def main(): + args = parser.parse_args() + dirpath = tempfile.mkdtemp() + rpmdb = path.join(dirpath, "var/lib/rpm") + try: + # Uncompress the latest database state into a temporary directory + for tar in args.uncompressed_layer: + with tarfile.open(tar, "r") as archive: + for member in archive.getmembers(): + if member.name.startswith(("/var/lib/rpm", "./var/lib/rpm")): + archive.extract(member, dirpath) + + # Add the rpm database if it is not there + subprocess.check_call(["rpm", "--dbpath", rpmdb, "--initdb"]) + + # Register the RPMs in the database + for rpm in args.rpm: + subprocess.check_call(["rpm", "--nosignature", "--dbpath", rpmdb, "-i", "-v", "--ignoresize", "--nodeps", "--noscripts" ,"--notriggers" ,"--excludepath", "/", rpm]) + + # Extract the rpms into the shared folder + for rpm in args.rpm: + p1 = subprocess.Popen(["rpm2cpio", rpm], stdout=subprocess.PIPE) + p2 = subprocess.Popen(["cpio", "-i", "-d", "-m", "-v", "-D", dirpath], stdin=p1.stdout, stdout=subprocess.PIPE) + p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits. + p2.communicate() + + with tarfile.open(args.output, "a") as tar: + tar.add(dirpath, arcname="/") + finally: + shutil.rmtree(dirpath) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/rpm/rpm.bzl b/rpm/rpm.bzl new file mode 100644 index 0000000..a870f90 --- /dev/null +++ b/rpm/rpm.bzl @@ -0,0 +1,52 @@ +load( + "@io_bazel_rules_docker//container:container.bzl", + _container = "container", +) +load( + "@io_bazel_rules_docker//container:layer_tools.bzl", + _get_layers = "get_from_target", +) + +def rpm_image(**kwargs): + _rpms_layer(**kwargs) + +def _rpms_impl(ctx, rpms = None): + rpms = rpms or ctx.files.rpms + rpm_installer = ctx.executable._rpm_installer + parent_parts = _get_layers(ctx, ctx.label.name, ctx.attr.base) + uncompressed_blobs = parent_parts.get("unzipped_layer", []) + uncompressed_layer_args = ["--uncompressed_layer=" + f.path for f in uncompressed_blobs] + rpm_args = ["--rpm=" + f.path for f in rpms] + finaltar = ctx.actions.declare_file(ctx.label.name + "-installed-rpms.tar") + target = "--output=%s" % finaltar.path + ctx.actions.run( + executable = rpm_installer, + arguments = rpm_args + uncompressed_layer_args + [target], + inputs = rpms + uncompressed_blobs, + outputs = [finaltar], + use_default_shell_env = True, + progress_message = "Install RPMs inside a container", + mnemonic = "installrpms", + ) + tars = [finaltar] + if ctx.attr.tars: + tars = tars + ctx.attr.tars + return _container.image.implementation(ctx, tars = tars) + +_rpms_layer = rule( + attrs = dict(_container.image.attrs.items() + { + # The dependency whose runfiles we're appending. + "rpms": attr.label_list(allow_files = True, mandatory = True), + "_rpm_installer": attr.label( + default = Label("//rpm:install_rpms"), + cfg = "host", + executable = True, + allow_files = True, + ), + }.items()), + executable = True, + outputs = _container.image.outputs, + # TODO: rpm toolchain? + #toolchains = ["@io_bazel_rules_docker//toolchains/docker:toolchain_type"], + implementation = _rpms_impl, +) diff --git a/test/BUILD b/test/BUILD new file mode 100644 index 0000000..f7d38e9 --- /dev/null +++ b/test/BUILD @@ -0,0 +1,19 @@ +load("@io_bazel_rules_docker//contrib:test.bzl", "container_test") + +container_test( + name = "image1_test", + configs = ["configs/image1.yaml"], + image = "//testdata:image1", +) + +container_test( + name = "image2_test", + configs = ["configs/image2.yaml"], + image = "//testdata:image2", +) + +container_test( + name = "allinone_test", + configs = ["configs/allinone.yaml"], + image = "//testdata:allinone", +) diff --git a/test/configs/allinone.yaml b/test/configs/allinone.yaml new file mode 100644 index 0000000..9d8359d --- /dev/null +++ b/test/configs/allinone.yaml @@ -0,0 +1,14 @@ +schemaVersion: '2.0.0' +fileExistenceTests: +- name: 'ldconfig binary from glibc' + path: '/sbin/ldconfig' + shouldExist: true + permissions: '-rwxr-xr-x' +- name: 'rpm database file' + path: '/var/lib/rpm/Packages' + shouldExist: true + permissions: '-rw-r--r--' +- name: 'readme from ca-certificates' + path: '/usr/share/pki/ca-trust-source/README' + shouldExist: true + permissions: '-rw-r--r--' \ No newline at end of file diff --git a/test/configs/image1.yaml b/test/configs/image1.yaml new file mode 100644 index 0000000..4668b14 --- /dev/null +++ b/test/configs/image1.yaml @@ -0,0 +1,13 @@ +schemaVersion: '2.0.0' +fileExistenceTests: +- name: 'ldconfig binary from glibc' + path: '/sbin/ldconfig' + shouldExist: true + permissions: '-rwxr-xr-x' +- name: 'rpm database file' + path: '/var/lib/rpm/Packages' + shouldExist: true + permissions: '-rw-r--r--' +- name: 'readme from ca-certificates' + path: '/usr/share/pki/ca-trust-source/README' + shouldExist: false diff --git a/test/configs/image2.yaml b/test/configs/image2.yaml new file mode 100644 index 0000000..9d8359d --- /dev/null +++ b/test/configs/image2.yaml @@ -0,0 +1,14 @@ +schemaVersion: '2.0.0' +fileExistenceTests: +- name: 'ldconfig binary from glibc' + path: '/sbin/ldconfig' + shouldExist: true + permissions: '-rwxr-xr-x' +- name: 'rpm database file' + path: '/var/lib/rpm/Packages' + shouldExist: true + permissions: '-rw-r--r--' +- name: 'readme from ca-certificates' + path: '/usr/share/pki/ca-trust-source/README' + shouldExist: true + permissions: '-rw-r--r--' \ No newline at end of file diff --git a/testdata/BUILD b/testdata/BUILD new file mode 100644 index 0000000..251a002 --- /dev/null +++ b/testdata/BUILD @@ -0,0 +1,35 @@ +package(default_visibility = ["//visibility:public"]) + +load( + "@io_bazel_rules_docker//container:container.bzl", + "container_image", +) + +load( + "//rpm:rpm.bzl", + "rpm_image", +) + +container_image( + name = "files_base", + files = ["foo"], + mode = "0o644", +) + +rpm_image( + name = "allinone", + base = ":files_base", + rpms = ["@glibc//file", "@ca_certificates//file"], +) + +rpm_image( + name = "image2", + base = ":image1", + rpms = ["@ca_certificates//file"], +) + +rpm_image( + name = "image1", + base = ":files_base", + rpms = ["@glibc//file"], +) diff --git a/testdata/foo b/testdata/foo new file mode 100644 index 0000000..5e40c08 --- /dev/null +++ b/testdata/foo @@ -0,0 +1 @@ +asdf \ No newline at end of file