From 348db1bdc16a0ae46211cd24959a5d7ba726e091 Mon Sep 17 00:00:00 2001 From: adam lazur Date: Fri, 16 Aug 2024 11:16:50 +0900 Subject: [PATCH 1/2] make the `oci_image()` macro wrapper accept a file for workdir This clones much of the same logic as `entrypoint` and friends. Also refactor docstring a little to illuminate "wrapper vs the rule" and the dual typed args. --- docs/image.md | 36 +++++++++++++++---------- examples/assertion/BUILD.bazel | 21 ++++++++++++++- oci/defs.bzl | 48 +++++++++++++++++++++++++--------- oci/private/image.bzl | 5 ++-- oci/private/image.sh | 2 +- 5 files changed, 81 insertions(+), 31 deletions(-) diff --git a/docs/image.md b/docs/image.md index 9b7a03c3..a04b9e60 100644 --- a/docs/image.md +++ b/docs/image.md @@ -85,7 +85,7 @@ oci_image( | user | The username or UID which is a platform-specific structure that allows specific control over which user the process run as. This acts as a default value to use when the value is not specified when creating a container. For Linux based systems, all of the following are valid: user, uid, user:group, uid:gid, uid:group, user:gid. If group/gid is not specified, the default group and supplementary groups of the given user/uid in /etc/passwd from the container are applied. | String | optional | "" | | variant | The variant of the specified CPU architecture. eg: v6, v7, v8. See: https://github.com/opencontainers/image-spec/blob/main/image-index.md#platform-variants for more. | String | optional | "" | | volumes | A file containing a comma separated list of volumes. (e.g. /srv/data,/srv/other-data) | Label | optional | None | -| workdir | Sets the current working directory of the entrypoint process in the container. This value acts as a default and may be replaced by a working directory specified when creating a container. | String | optional | "" | +| workdir | A file containing the path to the current working directory of the entrypoint process in the container. This value acts as a default and may be replaced by a working directory specified when runninng the container. | Label | optional | None | @@ -93,21 +93,28 @@ oci_image( ## oci_image
-oci_image(name, labels, annotations, env, cmd, entrypoint, exposed_ports, volumes, kwargs)
+oci_image(name, labels, annotations, env, cmd, entrypoint, workdir, exposed_ports, volumes, kwargs)
 
Macro wrapper around [oci_image_rule](#oci_image_rule). -Allows labels and annotations to be provided as a dictionary, in addition to a text file. -See https://github.com/opencontainers/image-spec/blob/main/annotations.md +This wrapper allows (some) parameters to be specified as a list or dict, or +as a target text file containing the contents. This accomodes rules to auto +generate some of these items. -Label/annotation/env can by configured using either dict(key->value) or a file that contains key=value pairs +Produces a target `[name].digest`, whose default output is a file containing the sha256 digest of the resulting image. +This is similar to the same-named target created by rules_docker's `container_image` macro. + +**DICT_OR_LABEL**: `label`, `annotation`, `env` + +Can by configured using either dict(key->value) or a file that contains key=value pairs (one per line). The file can be preprocessed using (e.g. using `jq`) to supply external (potentially not deterministic) information when running with `--stamp` flag. See the example in [/examples/labels/BUILD.bazel](https://github.com/bazel-contrib/rules_oci/blob/main/examples/labels/BUILD.bazel). -Produces a target `[name].digest`, whose default output is a file containing the sha256 digest of the resulting image. -This is similar to the same-named target created by rules_docker's `container_image` macro. +**LIST_OR_LABEL**: `cmd`, `entrypoint`, `workdir`, `exposed_ports`, `volumes` + +Can be a list of strings, or a file with newlines separating entries. **PARAMETERS** @@ -116,13 +123,14 @@ This is similar to the same-named target created by rules_docker's `container_im | Name | Description | Default Value | | :------------- | :------------- | :------------- | | name | name of resulting oci_image_rule | none | -| labels | Labels for the image config. See documentation above. | None | -| annotations | Annotations for the image config. See documentation above. | None | -| env | Environment variables provisioned by default to the running container. See documentation above. | None | -| cmd | Command & argument configured by default in the running container. See documentation above. | None | -| entrypoint | Entrypoint configured by default in the running container. See documentation above. | None | -| exposed_ports | Exposed ports in the running container. See documentation above. | None | -| volumes | Volumes for the container. See documentation above. | None | +| labels | DICT_OR_LABEL Labels for the image config. | None | +| annotations | DICT_OR_LABEL Annotations for the image config. | None | +| env | DICT_OR_LABEL Environment variables provisioned by default to the running container. | None | +| cmd | LIST_OR_LABEL Command & argument configured by default in the running container. | None | +| entrypoint | LIST_OR_LABEL Entrypoint configured by default in the running container. | None | +| workdir | LIST_OR_LABEL Workdir configured by default in the running container. Only 1 list entry allowed. | None | +| exposed_ports | LIST_OR_LABEL Exposed ports in the running container. | None | +| volumes | LIST_OR_LABEL Volumes for the container. | None | | kwargs | other named arguments to [oci_image_rule](#oci_image_rule) and [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). | none | diff --git a/examples/assertion/BUILD.bazel b/examples/assertion/BUILD.bazel index b8135340..72dbf299 100644 --- a/examples/assertion/BUILD.bazel +++ b/examples/assertion/BUILD.bazel @@ -190,7 +190,7 @@ oci_image( volumes = ["/srv/data"], # user & workdir user = "root", - workdir = "/root", + workdir = ["/root"], # labels & annotations labels = { "org.opencontainers.image.version": "0.0.0", @@ -619,3 +619,22 @@ build_test( ":case10_run", ], ) + +# Case 21: accept workdir param as a target (list input tested in case8) +genrule( + name = "case21_workdir_target", + outs = ["case21_workdir_target.txt"], + cmd = "echo -n /workdir > $@", +) + +oci_image( + name = "case21", + base = ":case8", + workdir = ":case21_workdir_target", +) + +assert_oci_config( + name = "test_case21", + image = ":case21", + workdir_eq = "/workdir", +) diff --git a/oci/defs.bzl b/oci/defs.bzl index bee35038..f6f0c1dd 100644 --- a/oci/defs.bzl +++ b/oci/defs.bzl @@ -33,29 +33,37 @@ def _write_nl_seperated_file(name, kind, elems, forwarded_kwargs): ) return label -def oci_image(name, labels = None, annotations = None, env = None, cmd = None, entrypoint = None, exposed_ports = None, volumes = None, **kwargs): +def oci_image(name, labels = None, annotations = None, env = None, cmd = None, entrypoint = None, workdir = None, exposed_ports = None, volumes = None, **kwargs): """Macro wrapper around [oci_image_rule](#oci_image_rule). - Allows labels and annotations to be provided as a dictionary, in addition to a text file. - See https://github.com/opencontainers/image-spec/blob/main/annotations.md + This wrapper allows (some) parameters to be specified as a list or dict, or + as a target text file containing the contents. This accomodes rules to auto + generate some of these items. - Label/annotation/env can by configured using either dict(key->value) or a file that contains key=value pairs + Produces a target `[name].digest`, whose default output is a file containing the sha256 digest of the resulting image. + This is similar to the same-named target created by rules_docker's `container_image` macro. + + **DICT_OR_LABEL**: `label`, `annotation`, `env` + + Can by configured using either dict(key->value) or a file that contains key=value pairs (one per line). The file can be preprocessed using (e.g. using `jq`) to supply external (potentially not deterministic) information when running with `--stamp` flag. See the example in [/examples/labels/BUILD.bazel](https://github.com/bazel-contrib/rules_oci/blob/main/examples/labels/BUILD.bazel). - Produces a target `[name].digest`, whose default output is a file containing the sha256 digest of the resulting image. - This is similar to the same-named target created by rules_docker's `container_image` macro. + **LIST_OR_LABEL**: `cmd`, `entrypoint`, `workdir`, `exposed_ports`, `volumes` + + Can be a list of strings, or a file with newlines separating entries. Args: name: name of resulting oci_image_rule - labels: Labels for the image config. See documentation above. - annotations: Annotations for the image config. See documentation above. - env: Environment variables provisioned by default to the running container. See documentation above. - cmd: Command & argument configured by default in the running container. See documentation above. - entrypoint: Entrypoint configured by default in the running container. See documentation above. - exposed_ports: Exposed ports in the running container. See documentation above. - volumes: Volumes for the container. See documentation above. + labels: `DICT_OR_LABEL` Labels for the image config. + annotations: `DICT_OR_LABEL` Annotations for the image config. + env: `DICT_OR_LABEL` Environment variables provisioned by default to the running container. + cmd: `LIST_OR_LABEL` Command & argument configured by default in the running container. + entrypoint: `LIST_OR_LABEL` Entrypoint configured by default in the running container. + workdir: `LIST_OR_LABEL` Workdir configured by default in the running container. Only 1 list entry allowed. + exposed_ports: `LIST_OR_LABEL` Exposed ports in the running container. + volumes: `LIST_OR_LABEL` Volumes for the container. **kwargs: other named arguments to [oci_image_rule](#oci_image_rule) and [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). """ @@ -107,6 +115,19 @@ def oci_image(name, labels = None, annotations = None, env = None, cmd = None, e forwarded_kwargs = forwarded_kwargs, ) + if types.is_list(workdir): + if len(workdir) > 1: + fail("workdir MUST only include 1 list element") + + workdir_label = "_{}_write_workdir".format(name) + write_file( + name = workdir_label, + out = "_{}.workdir.txt".format(name), + content = workdir, + **forwarded_kwargs + ) + workdir = workdir_label + if types.is_list(exposed_ports): exposed_ports_label = "_{}_write_exposed_ports".format(name) write_file( @@ -134,6 +155,7 @@ def oci_image(name, labels = None, annotations = None, env = None, cmd = None, e env = env, cmd = cmd, entrypoint = entrypoint, + workdir = workdir, exposed_ports = exposed_ports, volumes = volumes, **kwargs diff --git a/oci/private/image.bzl b/oci/private/image.bzl index 99558e91..1ae8fdad 100644 --- a/oci/private/image.bzl +++ b/oci/private/image.bzl @@ -81,7 +81,7 @@ This acts as a default value to use when the value is not specified when creatin For Linux based systems, all of the following are valid: `user`, `uid`, `user:group`, `uid:gid`, `uid:group`, `user:gid`. If `group/gid` is not specified, the default group and supplementary groups of the given `user/uid` in `/etc/passwd` from the container are applied. """), - "workdir": attr.string(doc = "Sets the current working directory of the `entrypoint` process in the container. This value acts as a default and may be replaced by a working directory specified when creating a container."), + "workdir": attr.label(doc = "A file containing the path to the current working directory of the `entrypoint` process in the container. This value acts as a default and may be replaced by a working directory specified when runninng the container.", allow_single_file = True), "exposed_ports": attr.label(doc = "A file containing a comma separated list of exposed ports. (e.g. 2000/tcp, 3000/udp or 4000. No protocol defaults to tcp).", allow_single_file = True), "volumes": attr.label(doc = "A file containing a comma separated list of volumes. (e.g. /srv/data,/srv/other-data)", allow_single_file = True), "os": attr.string(doc = "The name of the operating system which the image is built to run on. eg: `linux`, `windows`. See $GOOS documentation for possible values: https://go.dev/doc/install/source#environment"), @@ -227,7 +227,8 @@ def _oci_image_impl(ctx): args.add(ctx.attr.user, format = "--user=%s") if ctx.attr.workdir: - args.add(ctx.attr.workdir, format = "--workdir=%s") + args.add(ctx.file.workdir.path, format = "--workdir=%s") + inputs.append(ctx.file.workdir) action_env = {} diff --git a/oci/private/image.sh b/oci/private/image.sh index 13f1a480..28dfc322 100644 --- a/oci/private/image.sh +++ b/oci/private/image.sh @@ -164,7 +164,7 @@ for ARG in "$@"; do CONFIG=$(jq --arg user "${ARG#--user=}" '.config.User = $user' <<<"$CONFIG") ;; --workdir=*) - CONFIG=$(jq --arg workdir "${ARG#--workdir=}" '.config.WorkingDir = $workdir' <<<"$CONFIG") + CONFIG=$(jq --rawfile workdir "${ARG#--workdir=}" '.config.WorkingDir = $workdir' <<<"$CONFIG") ;; --labels=*) CONFIG=$(jq --rawfile labels "${ARG#--labels=}" '.config.Labels += ($labels | split("\n") | map(select(. | length > 0)) | map(. | split("=")) | map({key: .[0], value: .[1:] | join("=")}) | from_entries)' <<<"$CONFIG") From f9f94debe33c5ed84a47b101c629237c33b1d31d Mon Sep 17 00:00:00 2001 From: adam lazur Date: Mon, 2 Sep 2024 18:47:47 +0900 Subject: [PATCH 2/2] update `workdir` to be STRING_OR_LABEL It kinda doesn't exist in macro-land, so build a hacky string parsing heuristic --- docs/image.md | 8 ++++-- examples/assertion/BUILD.bazel | 6 ++--- oci/defs.bzl | 48 +++++++++++++++++++++++++++++----- oci/tests/BUILD.bazel | 3 +++ 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/docs/image.md b/docs/image.md index a04b9e60..6bdc2649 100644 --- a/docs/image.md +++ b/docs/image.md @@ -112,10 +112,14 @@ Can by configured using either dict(key->value) or a file that contains key=v deterministic) information when running with `--stamp` flag. See the example in [/examples/labels/BUILD.bazel](https://github.com/bazel-contrib/rules_oci/blob/main/examples/labels/BUILD.bazel). -**LIST_OR_LABEL**: `cmd`, `entrypoint`, `workdir`, `exposed_ports`, `volumes` +**LIST_OR_LABEL**: `cmd`, `entrypoint`, `exposed_ports`, `volumes` Can be a list of strings, or a file with newlines separating entries. +**STRING_OR_LABEL**: `workdir` + +A string, or a target text file whose output contains a single line + **PARAMETERS** @@ -128,7 +132,7 @@ Can be a list of strings, or a file with newlines separating entries. | env | DICT_OR_LABEL Environment variables provisioned by default to the running container. | None | | cmd | LIST_OR_LABEL Command & argument configured by default in the running container. | None | | entrypoint | LIST_OR_LABEL Entrypoint configured by default in the running container. | None | -| workdir | LIST_OR_LABEL Workdir configured by default in the running container. Only 1 list entry allowed. | None | +| workdir | STRING_OR_LABEL Workdir configured by default in the running container. | None | | exposed_ports | LIST_OR_LABEL Exposed ports in the running container. | None | | volumes | LIST_OR_LABEL Volumes for the container. | None | | kwargs | other named arguments to [oci_image_rule](#oci_image_rule) and [common rule attributes](https://bazel.build/reference/be/common-definitions#common-attributes). | none | diff --git a/examples/assertion/BUILD.bazel b/examples/assertion/BUILD.bazel index 72dbf299..fde1bddf 100644 --- a/examples/assertion/BUILD.bazel +++ b/examples/assertion/BUILD.bazel @@ -190,7 +190,7 @@ oci_image( volumes = ["/srv/data"], # user & workdir user = "root", - workdir = ["/root"], + workdir = "/root", # labels & annotations labels = { "org.opencontainers.image.version": "0.0.0", @@ -620,7 +620,7 @@ build_test( ], ) -# Case 21: accept workdir param as a target (list input tested in case8) +# Case 21: accept workdir param as a target label (string input is tested in case8) genrule( name = "case21_workdir_target", outs = ["case21_workdir_target.txt"], @@ -630,7 +630,7 @@ genrule( oci_image( name = "case21", base = ":case8", - workdir = ":case21_workdir_target", + workdir = "case21_workdir_target", ) assert_oci_config( diff --git a/oci/defs.bzl b/oci/defs.bzl index f6f0c1dd..5a8093d2 100644 --- a/oci/defs.bzl +++ b/oci/defs.bzl @@ -33,6 +33,38 @@ def _write_nl_seperated_file(name, kind, elems, forwarded_kwargs): ) return label + +# There's no "is this a label or just a string?" check in macro land, so +# approximate it with some silly string heuristics. See +# https://bazel.build/concepts/labels for label name rules +def _is_a_workdir_label(input): + if input[0] in (":", "@") or input[:2] in ("@@", "//") or '/' not in input: + return True + return False + + +def test_is_a_workdir_label(): + testdata = { + # Corner case: "foo" could be either a string or a target. This + # implementation chooses target. If you want a relative workdir "foo" + # use "./foo" + "foo": True, + "//foo": True, + "@@foo//bar": True, + ":foo": True, + # These are all not labels + "/foo": False, + "./foo": False, + "foo/bar": False, + "../foo": False, + } + + for input, expected in testdata.items(): + value = _is_a_workdir_label(input) + if value != expected: + fail("_is_a_workdir_label(%s) returned %s, expected %s" % (input, value, expected)) + + def oci_image(name, labels = None, annotations = None, env = None, cmd = None, entrypoint = None, workdir = None, exposed_ports = None, volumes = None, **kwargs): """Macro wrapper around [oci_image_rule](#oci_image_rule). @@ -50,10 +82,14 @@ def oci_image(name, labels = None, annotations = None, env = None, cmd = None, e deterministic) information when running with `--stamp` flag. See the example in [/examples/labels/BUILD.bazel](https://github.com/bazel-contrib/rules_oci/blob/main/examples/labels/BUILD.bazel). - **LIST_OR_LABEL**: `cmd`, `entrypoint`, `workdir`, `exposed_ports`, `volumes` + **LIST_OR_LABEL**: `cmd`, `entrypoint`, `exposed_ports`, `volumes` Can be a list of strings, or a file with newlines separating entries. + **STRING_OR_LABEL**: `workdir` + + A string, or a target text file whose output contains a single line + Args: name: name of resulting oci_image_rule labels: `DICT_OR_LABEL` Labels for the image config. @@ -61,7 +97,7 @@ def oci_image(name, labels = None, annotations = None, env = None, cmd = None, e env: `DICT_OR_LABEL` Environment variables provisioned by default to the running container. cmd: `LIST_OR_LABEL` Command & argument configured by default in the running container. entrypoint: `LIST_OR_LABEL` Entrypoint configured by default in the running container. - workdir: `LIST_OR_LABEL` Workdir configured by default in the running container. Only 1 list entry allowed. + workdir: `STRING_OR_LABEL` Workdir configured by default in the running container. exposed_ports: `LIST_OR_LABEL` Exposed ports in the running container. volumes: `LIST_OR_LABEL` Volumes for the container. **kwargs: other named arguments to [oci_image_rule](#oci_image_rule) and @@ -115,15 +151,13 @@ def oci_image(name, labels = None, annotations = None, env = None, cmd = None, e forwarded_kwargs = forwarded_kwargs, ) - if types.is_list(workdir): - if len(workdir) > 1: - fail("workdir MUST only include 1 list element") - + # Support a string for convenience. Create a label on the fly. + if workdir != None and not _is_a_workdir_label(workdir): workdir_label = "_{}_write_workdir".format(name) write_file( name = workdir_label, out = "_{}.workdir.txt".format(name), - content = workdir, + content = [workdir], **forwarded_kwargs ) workdir = workdir_label diff --git a/oci/tests/BUILD.bazel b/oci/tests/BUILD.bazel index 14819d04..2c558ed3 100644 --- a/oci/tests/BUILD.bazel +++ b/oci/tests/BUILD.bazel @@ -2,6 +2,7 @@ load("@aspect_bazel_lib//lib:diff_test.bzl", "diff_test") load("@aspect_bazel_lib//lib:run_binary.bzl", "run_binary") load("@bazel_skylib//rules:build_test.bzl", "build_test") load(":pull_tests.bzl", "parse_image_test") +load("//oci:defs.bzl", "test_is_a_workdir_label") IMAGES_TO_TEST = { "linux/amd64": { @@ -83,3 +84,5 @@ build_test( ) parse_image_test(name = "parse_image_test") + +test_is_a_workdir_label()