Make docker images fully reproducible [DI-741] - #1236
Conversation
|
|
||
| . .github/scripts/rhel.functions.sh | ||
|
|
||
| # Look up the existing image ID from Pyxis so downstream steps can still verify tags |
There was a problem hiding this comment.
Rather than trying-and-failing, could we lookup digest first and if not found then submit via preflight? Avoids ignoring preflight errors.
There was a problem hiding this comment.
Good suggestion and makes sense not to hide other errors
I agree if its possible.
If not possible then we should fail if there are errors other than is published, published image can't be updated
|
|
||
| . .github/scripts/rhel.functions.sh | ||
|
|
||
| # Look up the existing image ID from Pyxis so downstream steps can still verify tags |
There was a problem hiding this comment.
Good suggestion and makes sense not to hide other errors
I agree if its possible.
If not possible then we should fail if there are errors other than is published, published image can't be updated
|
|
||
| # Reproducible builds produce identical digests across rebuilds. | ||
| # If the same digest was already certified and published by a previous run, | ||
| # Pyxis rejects the submission with "is published, published image can't be updated". |
There was a problem hiding this comment.
Does this mean preflight check rejects already published images but continues with others?
as I don't see another call to preflight check
if thats the case then best to reflect in the comment as currently it implies the whole preflight fails
|
|
||
| echo "${PREFLIGHT_OUTPUT}" | ||
|
|
||
| # Reproducible builds produce identical digests across rebuilds. |
There was a problem hiding this comment.
Little confused about what IMAGE, IMAGE_IDS and DIGEST actually mean exactly
How does this relate to layers? Is a layer an IMAGE? Does each architecture equate to an IMAGE?
would be good if you can briefly explain how are these connected?
ps: i looked at Sprint Review diagram but still not sure
There was a problem hiding this comment.
| # Look up the existing image ID from Pyxis so downstream steps can still verify tags | ||
| DIGEST=$(grep -oP 'sha256:[a-f0-9]{64}' <<< "${PREFLIGHT_OUTPUT}" | head -1 || true) | ||
| if [[ -n "${DIGEST}" ]]; then | ||
| EXISTING_IMAGE_ID=$(get_image_id_by_digest \ |
There was a problem hiding this comment.
this implies there is only single previously published DIGEST?
But we now have multiple layers that might have been published previously
I think I need a lesson as little sketchy on this. More than happy to discuss and clarify in stand-up to save you typing
There was a problem hiding this comment.
Discussed on standup
nishaatr
left a comment
There was a problem hiding this comment.
@ldziedziul
I have done first pass
Have some clarification questions so will do second pass after that
Also, has this been proven to work in the field?
May be not possible or may be needs to go live once before we see the net effect in local builds, PR builds and rebuilds (when base image changes etc)?
Co-authored-by: Jack Green <jack.green@hazelcast.com>
Co-authored-by: Nishaat Rajabali <12186256+nishaatr@users.noreply.github.com>
Reproducible builds produce identical digests across rebuilds. Before running preflight, check Pyxis for an existing certified image with the same digest. If found, skip the entire certification flow (unique tag push, preflight scan, await publishing, tag copying) and only verify that tags are published.
| "${REDHAT_ROBOT_RHEL_API_KEY}") | ||
|
|
||
| if [[ -n "${EXISTING_IMAGE_ID}" && "${EXISTING_IMAGE_ID}" != "null" ]]; then | ||
| echonotice "Image ${{ vars.DOCKERHUB_NAMESPACE }}/${{ vars.PREPROD_REGISTRY }}-${{ vars.DOCKERHUB_EE_IMAGE_NAME }}:${{ steps.get-tags-to-push.outputs.primary-tag }} already published with image ID: ${EXISTING_IMAGE_ID}, skipping certification" |
There was a problem hiding this comment.
We should be explicit it's not just the same image, but identical digest.
Also we should specify we aren't just skipping certification - publish, too.
| PREFLIGHT_OUTPUT=$(preflight check container "${{ steps.scan_registry_secrets.outputs.SCAN_REPOSITORY }}:${{ steps.unique-destination-tag.outputs.tag }}" \ | ||
| --submit --pyxis-api-token=${REDHAT_ROBOT_RHEL_API_KEY} \ | ||
| --certification-component-id=${{ steps.scan_registry_secrets.outputs.PROJECT_ID }} \ | ||
| --docker-config ~/.docker/config.json \ | ||
| 2>&1) | ||
|
|
||
| echodebug "${PREFLIGHT_OUTPUT}" | ||
| echo "${PREFLIGHT_OUTPUT}" |
There was a problem hiding this comment.
I don't understand why we changed this - but I'm aware this whole thing (...that I wrote...) is bad as in the case of errors, you get no output at all.
There was a problem hiding this comment.
I think it's crucial information from this step and it's not that long
| . .github/scripts/rhel.functions.sh | ||
|
|
||
| jq --raw-output '.[]' <<< '${{ steps.preflight.outputs.IMAGE_IDS }}' | while read IMAGE_ID; do | ||
| jq --raw-output '.[]' <<< '${{ env.IMAGE_IDS }}' | while read IMAGE_ID; do |
There was a problem hiding this comment.
| jq --raw-output '.[]' <<< '${{ env.IMAGE_IDS }}' | while read IMAGE_ID; do | |
| jq --raw-output '.[]' <<< '${IMAGE_IDS}' | while read IMAGE_ID; do |
(biased, don't like env)
This uses the "escaped" version so is safer / less likely to break. And cleaner.
Fixed 2b2aa91 |
JackPGreen
left a comment
There was a problem hiding this comment.
I think we're nearly there.
| # Extract the linux/amd64 manifest digest, matching how the image is pushed | ||
| # to RedHat Container Registry (single-arch, not manifest list) | ||
| DIGEST=$(skopeo inspect --raw \ | ||
| docker://${FULL_PRIMARY_TAG} \ | ||
| | jq -r '.manifests[] | select(.platform.os == "linux" and .platform.architecture == "amd64") | .digest') | ||
| echo "Image ${FULL_PRIMARY_TAG} for linux/amd64 has digest: ${DIGEST}" |
There was a problem hiding this comment.
Could we avoid hardcoded platform here?
I.E. if we change the pushed platforms elsewhere this could break.
| --override-os linux \ | ||
| --override-arch amd64 \ |
There was a problem hiding this comment.
Why do we need this now? Or it just fluke that we happen to run this job on an amd64/linux platform?
There was a problem hiding this comment.
Because now we copy remaining tags using ${{ vars.DOCKERHUB_NAMESPACE }}/${{ vars.PREPROD_REGISTRY }}-${{ vars.DOCKERHUB_EE_IMAGE_NAME }}:${{ steps.get-tags-to-push.outputs.primary-tag }}(multi-arch) as the source, instead of ${{ steps.scan_registry_secrets.outputs.SCAN_REPOSITORY }}:${{ steps.unique-destination-tag.outputs.tag }} \. This way we can skip creation of unique tag when republishing the same content
|






Both OSS and Enterprise Dockerfiles now produce bit-for-bit identical images across clean builds. Every layer hash is deterministic regardless of when or where the build runs.
Fixes https://hazelcast.atlassian.net/browse/DI-741
Benefits
Changes
Reproducible distribution layer (OSS + EE)
/build_root/in a staging stage andCOPY --linkinto the final image, producing a platform-independent layer that is shared across architecturesSOURCE_DATE_EPOCHand--output type=docker,rewrite-timestamp=truesyntax=docker/dockerfile:1.7- enables BuildKit'sCOPY --linksupportEnterprise
OSS
apk upgradeinto a separate layer with its own timestamp normalizationSOURCE_DATE_EPOCHfor reproducibilityHow it works
COPY --linkflag makes the distribution layer independent of the base image, allowing it to be shared across platforms.rewrite-timestamp=truesets all files timestamps toSOURCE_DATE_EPOCHvalueVerification
EE
Build image (requires local
hazelcast-enterprise/hazelcast-enterprise-distribution-5.6.0.zip)Initial push image to JFrog - all layers need push
Clean local layers
Rebuild image from scratch
Push image to JFrog - all layers already exists
OSS
Build image (requires local
hazelcast-oss/hazelcast-distribution-5.6.0.zip)Initial push image to JFrog - all layers need push
Clean local layers
Rebuild image from scratch
Push image to JFrog - all layers already exists
Automated CI verification
A new reusable workflow (
.github/workflows/verify-layer-reproducibility.yml) runs on every PR viabuild-pr.yml. It builds each Dockerfile twice with--no-cacheand compares all layer digests usingdocker inspect. If any layer differs between builds, the job fails.