feat: Implement multi-arch build orchestration and image index assembly#2170
feat: Implement multi-arch build orchestration and image index assembly#2170anchi205 wants to merge 1 commit into
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Generate per-platform PipelineTasks with architecture-specific nodeSelectors so each build runs on a matching node. Source code is bundled as an OCI artifact during source-acquisition and pulled on each platform node. Per-platform tasks suffix the output image tag with the platform to prevent registry GC from deleting intermediate images. After all platform builds complete, an assemble-index task creates an OCI image index referencing each platform image. Result aggregation populates BuildRunStatus.platformResults with per-platform digest, size, vulnerabilities, and status, and sets status.output.digest to the image index digest. Partial failure is tracked per platform so users can identify which architecture failed. CLI flags --push-source-bundle and --assemble-index are added to the image-processing binary, consumed by the source-acquisition and assemble-index pipeline tasks respectively. Closes shipwright-io#2143, closes shipwright-io#2076 Signed-off-by: Anchita Borah <anborah@redhat.com>
d625a1c to
7ec0b3f
Compare
| It("should run vulnerability scanning on an image that is already pushed by the strategy", func() { | ||
| if testing.Short() { | ||
| Skip("skipping network-dependent test in -short mode") | ||
| } |
There was a problem hiding this comment.
This change is unrelated to the feature.
| logLogger := log.Logger{} | ||
| logLogger.SetOutput(GinkgoWriter) | ||
| s := httptest.NewServer(registry.New(registry.Logger(&logLogger))) | ||
| defer s.Close() | ||
| u, err := url.Parse(s.URL) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| endpoint := u.Host | ||
|
|
||
| amd64Tag := fmt.Sprintf("%s/test/app-linux-amd64:latest", endpoint) | ||
| arm64Tag := fmt.Sprintf("%s/test/app-linux-arm64:latest", endpoint) | ||
|
|
||
| amd64Ref, err := name.ParseReference(amd64Tag) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| arm64Ref, err := name.ParseReference(arm64Tag) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| Expect(remote.Write(amd64Ref, empty.Image)).To(Succeed()) | ||
| Expect(remote.Write(arm64Ref, empty.Image)).To(Succeed()) | ||
|
|
||
| amd64Digest, err := empty.Image.Digest() | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| arm64Digest, err := empty.Image.Digest() | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| outputRef := fmt.Sprintf("%s/test/app:latest", endpoint) | ||
| digestFile, err := os.CreateTemp("", "index-digest") | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| defer os.Remove(digestFile.Name()) |
There was a problem hiding this comment.
These are all setup-things that are better suited into a BeforeAll/Each node. DeferCleanup is the Ginkgo pattern to then delete after related Its ran.
In pkg/image/index_test.go for example you are setting up the registry server in a before node.
| {OS: "linux", Arch: "arm64", ImageRef: arm64Ref}, | ||
| } | ||
|
|
||
| idx, err := image.AssembleImageIndex(entries, nil) |
There was a problem hiding this comment.
At a first look, I had not expected this to work. The registry host is http. I assume only with options returned from GetOptions(insecure=true) this would be possible. But, instead GetOptions(insecure=false) builds something that actively prevents HTTP protocol. Maybe worth a comment that you can pass nil here.
| // spec; otherwise it's the output push secret (source bundle is pushed | ||
| // to the same registry as the output image). | ||
| bundlePullArgs := []string{ | ||
| "--image", "$(params.source-bundle-image)", |
There was a problem hiding this comment.
Can we stick to our params being prefixed with shp/ prefixParamsResultsVolumes ? And having a constant that we can reuse at declaration (further below) and usage.
| if p.Name == fmt.Sprintf("%s-%s", prefixParamsResultsVolumes, paramOutputImage) { | ||
| params[i].Value = pipelineapi.ParamValue{ | ||
| Type: pipelineapi.ParamTypeString, | ||
| StringVal: fmt.Sprintf("$(params.%s-%s)%s", prefixParamsResultsVolumes, paramOutputImage, platformImageTag(platform)), |
There was a problem hiding this comment.
This will only work as desired if the output image value is with tag, e. g. myimage:test as you then make myimage:test-linux-amd64 out of it. But for myimage, you make my-image-linux-amd64. This may still work as it is still a valid image name, but I assume we rather want my-image:latest-linux-amd64. Or do we have some normalization around this somewhere?
| // Build a clean base nodeSelector without os/arch keys | ||
| cleanBase := make(map[string]string, len(baseNodeSelector)) | ||
| for k, v := range baseNodeSelector { | ||
| if k != corev1.LabelOSStable && k != corev1.LabelArchStable { | ||
| cleanBase[k] = v | ||
| } | ||
| } | ||
|
|
||
| var specs []pipelineapi.PipelineTaskRunSpec | ||
| for _, p := range platforms { | ||
| ns := make(map[string]string, len(cleanBase)+2) | ||
| for k, v := range cleanBase { | ||
| ns[k] = v | ||
| } | ||
| ns[corev1.LabelOSStable] = p.OS | ||
| ns[corev1.LabelArchStable] = p.Arch |
There was a problem hiding this comment.
I find this quite complex. I think below is the same.
| // Build a clean base nodeSelector without os/arch keys | |
| cleanBase := make(map[string]string, len(baseNodeSelector)) | |
| for k, v := range baseNodeSelector { | |
| if k != corev1.LabelOSStable && k != corev1.LabelArchStable { | |
| cleanBase[k] = v | |
| } | |
| } | |
| var specs []pipelineapi.PipelineTaskRunSpec | |
| for _, p := range platforms { | |
| ns := make(map[string]string, len(cleanBase)+2) | |
| for k, v := range cleanBase { | |
| ns[k] = v | |
| } | |
| ns[corev1.LabelOSStable] = p.OS | |
| ns[corev1.LabelArchStable] = p.Arch | |
| var specs []pipelineapi.PipelineTaskRunSpec | |
| for _, p := range platforms { | |
| ns := maps.Close(baseNodeSelector) | |
| ns[corev1.LabelOSStable] = p.OS | |
| ns[corev1.LabelArchStable] = p.Arch |
| EmptyDir: &corev1.EmptyDirVolumeSource{}, | ||
| }, | ||
| { | ||
| Name: "cache", |
There was a problem hiding this comment.
Please use a constant for declaration and usage.
| result.Status = buildapi.PlatformBuildStatusFailed | ||
| result.FailureMessage = fmt.Sprintf("failed to fetch TaskRun %s: %v", childRef.Name, err) | ||
| buildRun.Status.PlatformResults = append(buildRun.Status.PlatformResults, result) | ||
| continue |
There was a problem hiding this comment.
I would rather return the error (unless NotFound) and reconcile again.
And what I wonder and am not sure out of my head ... does not Tekton inline parts of the TaskRun status into the PipelineRun status ? If so, is that something we can use here ?
| if err := c.Get(ctx, types.NamespacedName{ | ||
| Namespace: pipelineRun.Namespace, | ||
| Name: assembleRef.Name, | ||
| }, taskRun); err == nil { |
There was a problem hiding this comment.
Similar as above, is data available in the PipelineRun status and should we rather return the error (unless NotFound) and reconcile again?
There was a problem hiding this comment.
Pull request overview
This PR adds first-class multi-architecture build support to the BuildRun PipelineRun executor by fanning out per-platform build tasks (scheduled onto matching nodes), pushing/pulling source as an OCI artifact, and assembling a final OCI image index. It also extends status reporting to include per-platform results and updates docs/CRDs/tests accordingly.
Changes:
- Generate multi-arch PipelineRuns: per-platform
build-<os>-<arch>tasks +assemble-index, with per-task OS/arch nodeSelectors and EmptyDir workspaces. - Add image-processing CLI support for
--push-source-bundleand--assemble-index, plus anAssembleImageIndexhelper. - Report per-platform digests/sizes/vulnerabilities in
.status.platformResultsand set.status.output.digestto the index digest; add unit + e2e coverage and update docs/CRDs.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/utils/v1beta1/taskruns.go | Add helper to list/sort TaskRuns for a PipelineRun by label for e2e assertions. |
| test/utils/v1beta1/image.go | Fix digest validation for multi-platform outputs by using remote.Get (index-safe). |
| test/e2e/v1beta1/e2e_pipelinerun_test.go | Add e2e coverage for multi-arch spec.output.platforms and index assembly behavior. |
| test/e2e/v1beta1/common_suite_test.go | Extend test build prototype to set spec.output.platforms. |
| pkg/reconciler/buildrun/resources/results.go | Populate .status.platformResults and set output digest from assemble-index results. |
| pkg/reconciler/buildrun/resources/results_test.go | Add unit tests for multi-arch result extraction/aggregation. |
| pkg/reconciler/buildrun/resources/resource_builders.go | Add EmptyDir workspace bindings for multi-arch PipelineRuns. |
| pkg/reconciler/buildrun/resources/pipelinerun_generator.go | Implement multi-arch PipelineRun generation + scheduling pinning via TaskRunSpecs. |
| pkg/reconciler/buildrun/resources/multiarch.go | New: multi-arch pipeline task generation (source bundle push/pull, per-platform tasks, assemble-index). |
| pkg/reconciler/buildrun/resources/multiarch_test.go | New: unit tests for multi-arch pipeline generation contracts. |
| pkg/reconciler/buildrun/buildrun.go | Reconcile: extract per-platform results from PipelineRun child TaskRuns into BuildRun status. |
| pkg/image/index.go | New: build an OCI image index from per-platform images with platform descriptors. |
| pkg/image/index_test.go | Unit tests for index assembly behavior and error cases. |
| pkg/apis/build/v1beta1/zz_generated.deepcopy.go | DeepCopy updates for new PlatformResults / PlatformBuildResult. |
| pkg/apis/build/v1beta1/buildrun_types.go | API: define PlatformBuildResult and clarify output digest semantics for multi-arch. |
| docs/buildrun.md | Document .status.platformResults and multi-arch digest semantics. |
| docs/build.md | Document spec.output.platforms behavior/requirements and examples. |
| deploy/crds/shipwright.io_buildruns.yaml | CRD schema updates for new status fields and digest description. |
| cmd/image-processing/main.go | Add CLI modes for pushing source bundles and assembling/pushing image indexes. |
| cmd/image-processing/main_test.go | Add tests for new CLI modes and improve short-mode behavior for network-dependent test. |
Files not reviewed (1)
- pkg/apis/build/v1beta1/zz_generated.deepcopy.go: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| taskRun := &pipelineapi.TaskRun{} | ||
| if err := c.Get(ctx, types.NamespacedName{ | ||
| Namespace: pipelineRun.Namespace, | ||
| Name: childRef.Name, | ||
| }, taskRun); err != nil { | ||
| result.Status = buildapi.PlatformBuildStatusFailed | ||
| result.FailureMessage = fmt.Sprintf("failed to fetch TaskRun %s: %v", childRef.Name, err) | ||
| buildRun.Status.PlatformResults = append(buildRun.Status.PlatformResults, result) | ||
| continue | ||
| } |
| // Set output digest from the assemble-index task so the BuildRun reflects the manifest list, not a | ||
| // nondeterministic per-platform digest from the flat result aggregation. | ||
| // Output.Size is cleared because the flat aggregation picks it from an | ||
| // arbitrary per-platform TaskRun; correct per-platform data lives in | ||
| // PlatformResults. Output.Vulnerabilities is set to the union of all | ||
| // per-platform vulnerabilities. | ||
| if assembleRef, ok := childRefMap["assemble-index"]; ok { | ||
| taskRun := &pipelineapi.TaskRun{} | ||
| if err := c.Get(ctx, types.NamespacedName{ | ||
| Namespace: pipelineRun.Namespace, | ||
| Name: assembleRef.Name, | ||
| }, taskRun); err == nil { | ||
| if buildRun.Status.Output == nil { | ||
| buildRun.Status.Output = &buildapi.Output{} | ||
| } | ||
| for _, tr := range taskRun.Status.Results { | ||
| if tr.Name == digestResultName { | ||
| applyAssembleIndexOutputDigest(buildRun, tr.Value.StringVal) | ||
| } | ||
| } | ||
| buildRun.Status.Output.Size = 0 | ||
| buildRun.Status.Output.Vulnerabilities = allVulnerabilities | ||
| } | ||
| } | ||
| } |
| if g.isMultiArch() && !g.isSourceOCIArtifact() { | ||
| pushStep := generateSourceBundlePushStep(g.cfg, taskSpec, g.build.Spec.Output.PushSecret) | ||
| taskSpec.Steps = append(taskSpec.Steps, pushStep) | ||
| } |
| var secretVolumeMounts []corev1.VolumeMount | ||
| if build.Spec.Output.PushSecret != nil { | ||
| sources.AppendSecretVolume(taskSpec, *build.Spec.Output.PushSecret) | ||
| secretMountPath := fmt.Sprintf("/workspace/%s-push-secret", prefixParamsResultsVolumes) | ||
| secretVolumeMounts = append(secretVolumeMounts, corev1.VolumeMount{ |
| if err := CreateImageProcessingStep( | ||
| cfg, | ||
| taskSpec, | ||
| imgProcArgs, | ||
| false, | ||
| build.Spec.Output.PushSecret, | ||
| ); err != nil { | ||
| return pipelineapi.PipelineTask{}, fmt.Errorf("creating image processing step for %s: %w", taskName, err) | ||
| } |
Changes
Generate per-platform PipelineTasks with architecture-specific nodeSelectors so each build runs on a matching node. Source code is bundled as an OCI artifact during source-acquisition and pulled on each platform node. Per-platform tasks suffix the output image tag with the platform to prevent registry GC from deleting intermediate images.
After all platform builds complete, an assemble-index task creates an OCI image index referencing each platform image. Result aggregation populates BuildRunStatus.PlatformResults with per-platform digest, size, vulnerabilities, and status, and sets ManifestDigest and Output.Digest to the index digest. Partial failure is tracked per platform so users can identify which architecture failed.
CLI flags --push-source-bundle and --assemble-index are added to the image-processing binary, consumed by the source-acquisition and assemble-index pipeline tasks respectively.
Related Issues
Fixes #2143
Fixes #2076
Type of PR
/kind feature
Submitter Checklist
Release Notes