Repository: kubeflow/model-registry
Pull request #1114: Set up initial async job scaffolding
Author: login: Crazyglue
Labels: name: approved
name: lgtm
name: size/L
Created: 2025-05-15T14:19:27Z | Merged: 2025-05-20T08:01:37Z

<!--- Provide a general summary of your changes in the Title above -->

## Description

Based on the discussion found here: https://github.com/kubeflow/model-registry/issues/1108

This is the initial scaffolding needed to build a container to perform the async job described in #1108

It includes a relatively complete README and Dockerfile. Also, a pyproject file and placeholder python entrypoints and healthchecks.

The goal is to have the foundation in place before starting to write the actual logic of using the MR python client to perform async job.

## How Has This Been Tested?

Locally by executing:

```bash
docker build --build-arg VCS_REF=$(git rev-parse --short HEAD) --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') -t test/async-upload-job .
docker run test/async-upload-job
Hello, world!
```

## Merge criteria:
<!--- This PR will be merged by any repository approver when it meets all the points in the checklist -->
- All the commits have been [_signed-off_](https://github.com/kubeflow/community/tree/master/dco-signoff-hook#signing-off-commits)  (To pass the `DCO` check)

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [ ] The commits have meaningful messages; the author will squash them after approval or in case of manual merges will ask to merge with squash.
- [ ] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that the changes work.
- [ ] Code changes follow the [kubeflow contribution guidelines](https://www.kubeflow.org/docs/about/contributing/).
- [ ] **For first time contributors**: Please reach out to the [Reviewers](../OWNERS) to ensure all tests are being run, ensuring the label `ok-to-test` has been added to the PR.

If you have UI changes

<!--- You can ignore these if you are doing Go Model Registry REST server, MR Python client, manifest, controller, internal logic, etc changes; aka non-UI / visual changes -->
- [ ] The developer has added tests or explained why testing cannot be added.
- [ ] Included any necessary screenshots or gifs if it was a UI change.
- [ ] Verify that UI/UX changes conform the UX guidelines for Kubeflow.



Diff:
diff --git a/jobs/async-upload/.dockerignore b/jobs/async-upload/.dockerignore
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jobs/async-upload/Dockerfile b/jobs/async-upload/Dockerfile
new file mode 100644
index 0000000000..063b7c2d2f
--- /dev/null
+++ b/jobs/async-upload/Dockerfile
@@ -0,0 +1,50 @@
+###############################################################################
+#  Asynchronous Model-Sync Job image for Kubeflow Model Registry (https://github.com/kubeflow/model-registry/issues/1108)
+###############################################################################
+
+FROM ubi10/python-312-minimal AS base
+
+# security/env hardening
+ENV PYTHONDONTWRITEBYTECODE=1 \
+    PYTHONUNBUFFERED=1 \
+    PIP_NO_CACHE_DIR=off \
+    PIP_DISABLE_PIP_VERSION_CHECK=on
+
+# Create an unprivileged user
+RUN addgroup --system --gid 1000 app \
+    && adduser  --system --uid 1000 --ingroup app --home /app app
+
+WORKDIR /app
+
+# Copy Python dependencies first to leverage layer caching
+COPY requirements.txt .
+RUN --mount=type=cache,target=/root/.cache \
+    apt-get update && apt-get install -y --no-install-recommends ca-certificates \
+    && pip install --no-cache-dir -r requirements.txt \
+    && apt-get purge -y --auto-remove && rm -rf /var/lib/apt/lists/*
+
+# Copy application source
+COPY . .
+
+# Switch to the non-root user for the final image layer
+USER 1000:1000
+
+# Add an explicit health-check (K8s will surface this in Pod.status)
+HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
+    CMD python -m job.healthcheck
+
+# OCI-recommended labels
+ARG VCS_REF=unknown
+ARG BUILD_DATE
+LABEL org.opencontainers.image.created=$BUILD_DATE \
+    org.opencontainers.image.revision=$VCS_REF \
+    org.opencontainers.image.title="Kubeflow Model Registry Async-Upload Job" \
+    org.opencontainers.image.description="K8s Job image that copies a model between storage back-ends and registers it in Kubeflow Model Registry." \
+    org.opencontainers.image.url="https://github.com/kubeflow/model-registry" \
+    org.opencontainers.image.source="https://github.com/kubeflow/model-registry/tree/main/jobs/async-upload" \
+    org.opencontainers.image.version="0.1.0" \
+    org.opencontainers.image.licenses="Apache-2.0" \
+    org.opencontainers.image.authors="Kubeflow Model Registry maintainers"
+
+# The Job controller will pass CLI flags/ENV-vars described in issue #1108.
+ENTRYPOINT ["python", "-m", "job.entrypoint"]
diff --git a/jobs/async-upload/README.md b/jobs/async-upload/README.md
new file mode 100644
index 0000000000..443ed1c843
--- /dev/null
+++ b/jobs/async-upload/README.md
@@ -0,0 +1,26 @@
+# Kubeflow Model Registry – Async Upload Job
+
+> Lightweight, non‑root Python Job containerised for **transferring model artefacts** between storage back‑ends (S3, OCI, PVC, …) and registering them in **Kubeflow Model Registry**. Born out of the discussion in [kubeflow/model-registry #1108](https://github.com/kubeflow/model-registry/issues/1108).
+
+---
+
+## Quick start
+
+```bash
+# 1 – Build & tag ( reproducible build args are optional )
+docker build \
+  --build-arg VCS_REF=$(git rev-parse --short HEAD) \
+  --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
+  -t quay.io/<org>/async-upload-job:$(git rev-parse --short HEAD) .
+
+# 2 – Push to your registry
+docker push quay.io/<org>/async-upload-job:<tag>
+
+# TODO: Run locally...
+```
+
+## References
+
+- Issue thread : [https://github.com/kubeflow/model-registry/issues/1108](https://github.com/kubeflow/model-registry/issues/1108)
+- OCI Image Spec : [https://github.com/opencontainers/image-spec](https://github.com/opencontainers/image-spec)
+- Kubernetes Pod Security : [https://kubernetes.io/docs/concepts/security/pod-security-standards/](https://kubernetes.io/docs/concepts/security/pod-security-standards/)
diff --git a/jobs/async-upload/job/__init__.py b/jobs/async-upload/job/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jobs/async-upload/job/entrypoint.py b/jobs/async-upload/job/entrypoint.py
new file mode 100644
index 0000000000..723e3dc7b6
--- /dev/null
+++ b/jobs/async-upload/job/entrypoint.py
@@ -0,0 +1,11 @@
+def main() -> None:
+    """
+    Placeholder for main entrypoint
+
+    TODO: Implement the main entrypoint
+    """
+    print("Hello, world!")
+
+
+if __name__ == "__main__":  # pragma: no cover
+    main()
diff --git a/jobs/async-upload/job/healthcheck.py b/jobs/async-upload/job/healthcheck.py
new file mode 100644
index 0000000000..5ff9dd8517
--- /dev/null
+++ b/jobs/async-upload/job/healthcheck.py
@@ -0,0 +1,14 @@
+import sys
+
+
+def main() -> None:
+    """
+    Placeholder for a healthcheck endpoint for the k8s api server to validate the job is still running
+
+    TODO: Implement healthchecks
+    """
+    sys.exit(0)
+
+
+if __name__ == "__main__":  # pragma: no cover
+    main()
diff --git a/jobs/async-upload/pyproject.toml b/jobs/async-upload/pyproject.toml
new file mode 100644
index 0000000000..b4745dbeee
--- /dev/null
+++ b/jobs/async-upload/pyproject.toml
@@ -0,0 +1,16 @@
+[project]
+name = "async-upload"
+version = "0.0.1"
+description = "Model Registry Job to upload a Model from a source to a destination"
+authors = [
+    {name = "Your Name",email = "you@example.com"}
+]
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+]
+
+
+[build-system]
+requires = ["poetry-core>=2.0.0,<3.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/jobs/async-upload/requirements.txt b/jobs/async-upload/requirements.txt
new file mode 100644
index 0000000000..e69de29bb2


Reviews:
user: login: tarilabs
body: thanks @Crazyglue , as requested here is a couple of early comments 👍 
we can work on this capability iteratively like we did for csi and like we're doing for the Isvc reconciler
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T06:16:12Z
user: login: Crazyglue
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T13:28:14Z

Review comments:
diff_hunk: @@ -0,0 +1,43 @@
+# Kubeflow Model Registry – Async Upload Job
+
+> Lightweight, non‑root Python Job containerised for **transferring model artefacts** between storage back‑ends (S3, OCI, PVC, …) and registering them in **Kubeflow Model Registry**. Born out of the discussion in [kubeflow/model-registry #1108](https://github.com/kubeflow/model-registry/issues/1108).
+
+---
+
+## Quick start
+
+```bash
+# 1 – Build & tag ( reproducible build args are optional )
+docker build \
+  --build-arg VCS_REF=$(git rev-parse --short HEAD) \
+  --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
+  -t quay.io/<org>/async-upload-job:$(git rev-parse --short HEAD) .
+
+# 2 – Push to your registry
+docker push quay.io/<org>/async-upload-job:<tag>
+
+# TODO: Run locally...
+```
+
+---
+
+## Security & supply‑chain
+
+- **Non‑root runtime**
+- **Distroless‑ish** slim base, no build tools in final layer
+- **SBOM generation** – use `syft packages dir:.` in CI
+- **Image signing** – add `cosign sign --key cosign.key <image>`
+
+---
+
+## License
+
+Licensed under the **Apache License 2.0** – see [`LICENSE`](../../LICENSE) in root for details.
+
+---
path: jobs/async-upload/README.md
user: login: tarilabs
body: I don't think this part is needed, because we have already coverage from the main repo (I would avoid duplication), further I don't think we're cosigning container images ootb
created_at: 2025-05-16T06:11:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,50 @@
+###############################################################################
+#  Asynchronous Model-Sync Job image for Kubeflow Model Registry (https://github.com/kubeflow/model-registry/issues/1108)
+###############################################################################
+
+FROM python:3.12.10-slim AS base
path: jobs/async-upload/Dockerfile
user: login: tarilabs
body: how about using `ubi10/python-312-minimal` ?
created_at: 2025-05-16T06:13:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,50 @@
+###############################################################################
+#  Asynchronous Model-Sync Job image for Kubeflow Model Registry (https://github.com/kubeflow/model-registry/issues/1108)
+###############################################################################
+
+FROM python:3.12.10-slim AS base
path: jobs/async-upload/Dockerfile
user: login: Crazyglue
body: Was not aware that this existed! I'll switch it over
created_at: 2025-05-16T13:28:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: tarilabs
created_at: 2025-05-20T08:01:19Z
body: > This is the initial scaffolding needed to build a container to perform the async job described in #1108

> The goal is to have the foundation in place before starting to write the actual logic of using the MR python client to perform async job.

merging on scaffolding request from author

/lgtm
/approve
author_association: MEMBER
user: login: google-oss-prow[bot]
created_at: 2025-05-20T08:01:27Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeflow/model-registry/pull/1114#issuecomment-2893350874" title="Approved">tarilabs</a>*

The full list of commands accepted by this bot can be found [here](https://oss.gprow.dev/command-help?repo=kubeflow%2Fmodel-registry).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubeflow/model-registry/blob/main/OWNERS)~~ [tarilabs]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR