Skip to content

Commit

Permalink
feat: implement oci_push (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
thesayyn authored Jan 3, 2023
1 parent a3bf6e7 commit 202890e
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ stardoc_with_diff_test(
bzl_library_target = "//oci/private:image_index",
)

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

stardoc_with_diff_test(
name = "cosign_sign",
bzl_library_target = "//cosign/private:sign",
Expand Down
80 changes: 80 additions & 0 deletions docs/push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!-- Generated with Stardoc: http://skydoc.bazel.build -->

Implementation details for the push rule

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

## oci_push

<pre>
oci_push(<a href="#oci_push-name">name</a>, <a href="#oci_push-default_tags">default_tags</a>, <a href="#oci_push-image">image</a>, <a href="#oci_push-repository">repository</a>)
</pre>

Push an oci_image or oci_image_index to a remote registry.

Pushing and tagging are performed sequentially which MAY lead to non-atomic pushes if one the following events occur;

- Remote registry rejects a tag due to various reasons. eg: forbidden characters, existing tags
- Remote registry closes the connection during the tagging
- Local network outages

In order to avoid incomplete pushes oci_push will push the image by its digest and then apply the `default_tags` sequentially at
the remote registry.

Any failure during pushing or tagging will be reported with non-zero exit code cause remaining steps to be skipped.


Push an oci_image to docker registry with latest tag

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

oci_push(
image = ":image",
repository = "index.docker.io/<ORG>/image",
default_tags = ["latest"]
)
```

Push an oci_image_index to github container registry with a semver tag

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

oci_image(name = "app_linux_amd64")

oci_image(name = "app_windows_amd64")

oci_image_index(
name = "app_image",
images = [
":app_linux_arm64",
":app_linux_amd64",
":app_windows_amd64",
]
)

oci_push(
image = ":app_image",
repository = "ghcr.io/<OWNER>/image",
default_tags = ["0.0.0"]
)
```

Ideally the semver information is gathered from a vcs, like git, instead of being hardcoded to the BUILD files.
However, due to nature of BUILD files being static, one has to use `-t|--tag` flag to pass the tag at runtime instead of using `default_tags`. eg. `bazel run //target:push -- --tag $(git tag)`

Similary, the `repository` attribute can be overridden at runtime with the `-r|--repository` flag. eg. `bazel run //target:push -- --repository index.docker.io/<ORG>/image`


**ATTRIBUTES**


| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="oci_push-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_push-default_tags"></a>default_tags | List of tags to apply to the image at remote registry. | List of strings | optional | [] |
| <a id="oci_push-image"></a>image | Label to an oci_image or oci_image_index | <a href="https://bazel.build/docs/build-ref.html#labels">Label</a> | optional | None |
| <a id="oci_push-repository"></a>repository | Repository URL where the image will be signed at. eg: index.docker.io/&lt;user&gt;/image. digests and tags are disallowed. | String | required | |


59 changes: 59 additions & 0 deletions example/push/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
load("//oci:defs.bzl", "oci_image", "oci_image_index", "oci_push")

oci_image(
name = "image",
architecture = "amd64",
entrypoint = ["/fail"],
os = "linux",
)

oci_push(
name = "push_image",
image = ":image",
repository = "index.docker.io/<ORG>/image",
default_tags = ["latest"]
)

oci_push(
name = "push_image_wo_tags",
image = ":image",
repository = "index.docker.io/<ORG>/image"
)

oci_image_index(
name = "image_index",
images = [
":image"
]
)

oci_push(
name = "push_image_index",
image = ":image_index",
repository = "index.docker.io/<ORG>/image",
default_tags = ["nightly"]
)


sh_test(
name = "test",
srcs = ["test.bash"],
args = [
"$(CRANE_BIN)",
"$(LAUNCHER)",
"$(location :push_image)",
"$(location :push_image_index)",
"$(location :push_image_wo_tags)"
],
data = [
":push_image",
":push_image_index",
":push_image_wo_tags",
"@oci_crane_toolchains//:current_toolchain",
"@oci_zot_toolchains//:current_toolchain",
],
toolchains = [
"@oci_crane_toolchains//:current_toolchain",
"@oci_zot_toolchains//:current_toolchain",
],
)
47 changes: 47 additions & 0 deletions example/push/test.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -o pipefail -o errexit -o nounset

readonly CRANE="$1"
readonly REGISTRY_LAUNCHER="$2"

# Launch a registry instance at a random port
source "${REGISTRY_LAUNCHER}"
start_registry $TEST_TMPDIR $TEST_TMPDIR/output.log
echo "Registry is running at ${REGISTRY}"


readonly PUSH_IMAGE="$3"
readonly PUSH_IMAGE_INDEX="$4"
readonly PUSH_IMAGE_WO_TAGS="$5"


# should push image with default tags
REPOSITORY="${REGISTRY}/local"
"${PUSH_IMAGE}" --repository "${REPOSITORY}"
"${CRANE}" digest "$REPOSITORY:latest"

# should push image_index with default tags
REPOSITORY="${REGISTRY}/local-index"
"${PUSH_IMAGE_INDEX}" --repository "${REPOSITORY}"
"${CRANE}" digest "$REPOSITORY:nightly"


# should push image without default tags
REPOSITORY="${REGISTRY}/local-wo-tags"
"${PUSH_IMAGE_WO_TAGS}" --repository "${REPOSITORY}"
TAGS=$("${CRANE}" ls "$REPOSITORY")
if [ -n "${TAGS}" ]; then
echo "image is not supposed to have any tags but got"
echo "${TAGS}"
exit 1
fi

# should push image with the --tag flag.
REPOSITORY="${REGISTRY}/local-flag-tag"
"${PUSH_IMAGE_WO_TAGS}" --repository "${REPOSITORY}" --tag "custom"
TAGS=$("${CRANE}" ls "$REPOSITORY")
if [ "${TAGS}" != "custom" ]; then
echo "image is supposed to have custom tag but got"
echo "${TAGS}"
exit 1
fi
2 changes: 2 additions & 0 deletions oci/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
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:push.bzl", _oci_push = "oci_push")
load("//oci/private:structure_test.bzl", _structure_test = "structure_test")

oci_tarball = _oci_tarball
oci_image = _oci_image
oci_image_index = _oci_image_index
oci_push = _oci_push
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 @@ -9,6 +9,7 @@ exports_files([
"image.sh.tpl",
"image_index.sh.tpl",
"tarball.sh.tpl",
"push.sh.tpl"
])

filegroup(
Expand Down Expand Up @@ -48,6 +49,15 @@ bzl_library(
],
)

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

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

_DOC = """Push an oci_image or oci_image_index to a remote registry.
Pushing and tagging are performed sequentially which MAY lead to non-atomic pushes if one the following events occur;
- Remote registry rejects a tag due to various reasons. eg: forbidden characters, existing tags
- Remote registry closes the connection during the tagging
- Local network outages
In order to avoid incomplete pushes oci_push will push the image by its digest and then apply the `default_tags` sequentially at
the remote registry.
Any failure during pushing or tagging will be reported with non-zero exit code cause remaining steps to be skipped.
Push an oci_image to docker registry with latest tag
```starlark
oci_image(name = "image")
oci_push(
image = ":image",
repository = "index.docker.io/<ORG>/image",
default_tags = ["latest"]
)
```
Push an oci_image_index to github container registry with a semver tag
```starlark
oci_image(name = "app_linux_arm64")
oci_image(name = "app_linux_amd64")
oci_image(name = "app_windows_amd64")
oci_image_index(
name = "app_image",
images = [
":app_linux_arm64",
":app_linux_amd64",
":app_windows_amd64",
]
)
oci_push(
image = ":app_image",
repository = "ghcr.io/<OWNER>/image",
default_tags = ["0.0.0"]
)
```
Ideally the semver information is gathered from a vcs, like git, instead of being hardcoded to the BUILD files.
However, due to nature of BUILD files being static, one has to use `-t|--tag` flag to pass the tag at runtime instead of using `default_tags`. eg. `bazel run //target:push -- --tag $(git tag)`
Similary, the `repository` attribute can be overridden at runtime with the `-r|--repository` flag. eg. `bazel run //target:push -- --repository index.docker.io/<ORG>/image`
"""
_attrs = {
"image": attr.label(allow_single_file = True, doc = "Label to an oci_image or oci_image_index"),
"repository": attr.string(mandatory = True, doc = "Repository URL where the image will be signed at. eg: index.docker.io/<user>/image. digests and tags are disallowed."),
"default_tags": attr.string_list(doc = "List of tags to apply to the image at remote registry."),
"_push_sh_tpl": attr.label(default = "push.sh.tpl", allow_single_file = True),
}

def _quote_args(args):
return ["\"{}\"".format(arg) for arg in args]

def _impl(ctx):
crane = ctx.toolchains["@contrib_rules_oci//oci:crane_toolchain_type"]
jq = ctx.toolchains["@aspect_bazel_lib//lib:yq_toolchain_type"]

if not ctx.file.image.is_directory:
fail("image attribute must be a oci_image or oci_image_index")

if ctx.attr.repository.find(":") != -1 or ctx.attr.repository.find("@") != -1:
fail("repository attribute should not contain digest or tag.")

fixed_args = ["--tag={}".format(tag) for tag in ctx.attr.default_tags]
fixed_args.extend(["--repository", ctx.attr.repository])

executable = ctx.actions.declare_file("push_%s.sh" % ctx.label.name)
ctx.actions.expand_template(
template = ctx.file._push_sh_tpl,
output = executable,
is_executable = True,
substitutions = {
"{{crane_path}}": crane.crane_info.crane_path,
"{{yq_path}}": jq.yqinfo.bin.short_path,
"{{image_dir}}": ctx.file.image.short_path,
"{{fixed_args}}": " ".join(_quote_args(fixed_args)),
},
)

runfiles = ctx.runfiles(files = [ctx.file.image])
runfiles = runfiles.merge(jq.default.default_runfiles)
runfiles = runfiles.merge(crane.default.default_runfiles)

return DefaultInfo(executable = executable, runfiles = runfiles)

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

readonly CRANE="{{crane_path}}"
readonly YQ="{{yq_path}}"
readonly IMAGE_DIR="{{image_dir}}"
readonly FIXED_ARGS=({{fixed_args}})

# set $@ to be FIXED_ARGS+$@
ALL_ARGS=(${FIXED_ARGS[@]} $@)
set -- ${ALL_ARGS[@]}

REPOSITORY="{{repository}}"
TAGS=()
ARGS=()

while (( $# > 0 )); do
case $1 in
(-t|--tag)
TAGS+=( "$2" )
shift
shift;;
(--tag=*)
TAGS+=( "${1#--tag=}" )
shift;;
(-r|--repository)
REPOSITORY="$2"
shift
shift;;
(--repository=*)
REPOSITORY="${1#--repository=}"
shift;;
(*)
ARGS+=( "$1" )
shift;;
esac
done

DIGEST=$("${YQ}" eval '.manifests[0].digest' "${IMAGE_DIR}/index.json")

REFS=$(mktemp)
"${CRANE}" push "${IMAGE_DIR}" "${REPOSITORY}@${DIGEST}" "${ARGS[@]+"${ARGS[@]}"}" --image-refs "${REFS}"

for tag in "${TAGS[@]+"${TAGS[@]}"}"
do
"${CRANE}" tag $(cat "${REFS}") "${tag}"
done

0 comments on commit 202890e

Please sign in to comment.