feat: add SoftwareBuild CRD for multi-OS software builds#199
Conversation
Add a new SoftwareBuild custom resource that enables building software for arbitrary target OSes using user-specified container images and five sequential shell command stages (fetch, prebuild, build, postbuild, deploy). This complements the existing ImageBuild CRD (which uses AIB for automotive OS images) with a generic, stage-based build model that supports any toolchain: Zephyr RTOS, Ubuntu, OpenBSW, etc. Changes: - New CRD: SoftwareBuild (api/v1alpha1/softwarebuild_types.go) - New controller: SoftwareBuildReconciler with PipelineRun lifecycle - New Tekton pipeline generation: software-build-pipeline with five sequential tasks running inside the user's container image - OperatorConfig: add softwareBuilds section to enable the pipeline - Wire controller into cmd/main.go under build/all mode - Unit tests for API types, pipeline generation, and controller - Envtest integration tests following upstream Ginkgo patterns - Sample CRs for Ubuntu and Zephyr RTOS builds Made-with: Cursor
The generated CRD YAML existed but was not listed in config/crd/kustomization.yaml, so make install never applied it. Made-with: Cursor
On Kind/vanilla k8s, SecurityContextConstraints CRD does not exist. The SCC delete in cleanupWorkspaceInfra returned a NoMatchError which blocked the entire OperatorConfig reconciliation, preventing the software-build-pipeline from being deployed. Now treat NoMatchError the same as NotFound. Made-with: Cursor
- run-pipeline.sh was reading .status.currentPipelineRun but the CRD field is .status.pipelineRunName - Controller now handles AlreadyExists when creating a PipelineRun, which happens if the status update after creation fails and the reconcile loop retries Made-with: Cursor
- Prepend git clone to fetch command when source.type is git, cloning into src/ so west/cmake workspace metadata persists on the PVC - Set imagePullPolicy to IfNotPresent so pre-loaded images work in Kind Made-with: Cursor
…nding Allow run-pipeline.sh to accept a Kustomize overlay directory in addition to individual CR files, enabling a single CR definition with environment- specific overlays (local dev vs CI). Add PVC workspace binding support so SoftwareBuilds with source.type=pvc attach to an existing PVC instead of always creating a new VolumeClaimTemplate. Made-with: Cursor
📝 WalkthroughWalkthroughAdds a namespaced SoftwareBuild CRD and controller, extends OperatorConfig with SoftwareBuilds settings, generates Tekton Pipeline/PipelineRun and PVC handling, wires the controller in main, adds samples/docs, updates deepcopy/codegen, and includes unit and integration tests. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant K8s as Kubernetes API
participant Controller as SoftwareBuild Controller
participant OpConfig as OperatorConfig Controller
participant TaskGen as Task Generator
participant Tekton as Tekton API
User->>K8s: Create SoftwareBuild
K8s->>Controller: Reconcile request
alt no PipelineRunName
Controller->>OpConfig: Read OperatorConfig (build defaults)
OpConfig-->>Controller: BuildConfig (PVCSize, timeout, DefaultImage)
Controller->>TaskGen: Generate Pipeline & PipelineRun (params, workspace, SA)
TaskGen-->>Controller: PipelineRun manifest
Controller->>Tekton: Create PipelineRun
Tekton-->>Controller: PipelineRun created
Controller->>K8s: Patch SoftwareBuild.Status (PipelineRunName, Phase=Pending)
else existing PipelineRunName
Controller->>Tekton: Get PipelineRun (operator NS or SB NS)
Tekton-->>Controller: PipelineRun status/conditions
Controller->>Controller: Map PipelineRun -> SoftwareBuild.Status (Phase, Stages, FailureReason)
Controller->>K8s: Update SoftwareBuild.Status
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
internal/controller/softwarebuild/controller.go (3)
62-69: Silent error handling on status update.When a
PipelineRunalready exists, the status update error is silently discarded. If the update fails, subsequent reconciliation may behave incorrectly as thePipelineRunNamewon't be persisted.♻️ Proposed fix to log or handle the error
if err := r.Create(ctx, pr); err != nil { if errors.IsAlreadyExists(err) { sb.Status.PipelineRunName = pr.Name - _ = r.Status().Update(ctx, sb) + if updateErr := r.Status().Update(ctx, sb); updateErr != nil { + logger.Error(updateErr, "failed to update status after PipelineRun already exists") + return ctrl.Result{}, updateErr + } return ctrl.Result{Requeue: true}, nil } return ctrl.Result{}, fmt.Errorf("creating PipelineRun: %w", err) }Note: The
loggervariable needs to be passed to or accessible withincreatePipelineRun.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/controller/softwarebuild/controller.go` around lines 62 - 69, The status update error after detecting an existing PipelineRun is currently ignored; in createPipelineRun, capture the error returned by r.Status().Update(ctx, sb) when setting sb.Status.PipelineRunName = pr.Name and either log it or return it so failures are not silent. Pass a logger into createPipelineRun (or make logger accessible) and replace the blank assignment "_ = r.Status().Update(...)" with proper error handling: check the returned error from r.Status().Update, call logger.Error/Infof with context including sb.Name and the error (or return a wrapped error) to ensure the update failure is visible and reconciliation can react accordingly.
179-191:loadBuildConfigsilently returns nil on errors.If the
OperatorConfigfetch fails for reasons other than "not found" (e.g., network issues, RBAC), the error is silently ignored and defaults are used. This could mask configuration issues.♻️ Proposed fix to distinguish error types
func (r *SoftwareBuildReconciler) loadBuildConfig(ctx context.Context, namespace string) *tasks.BuildConfig { var opConfig automotivev1alpha1.OperatorConfig if err := r.Get(ctx, types.NamespacedName{Name: "default", Namespace: namespace}, &opConfig); err != nil { + if !errors.IsNotFound(err) { + // Log unexpected errors for debugging + r.Log.Error(err, "failed to load OperatorConfig, using defaults") + } return nil }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/controller/softwarebuild/controller.go` around lines 179 - 191, The loadBuildConfig function currently swallows any error from r.Get; change it to distinguish NotFound from other errors by using k8s.io/apimachinery/pkg/api/errors (apierrors.IsNotFound): if the get returns NotFound, return nil as before; if it returns any other error, log the error via the reconciler logger (e.g., r.Log.Error(err, "failed to get OperatorConfig", "name", "default", "namespace", namespace)) and return nil (or propagate the error from the caller if preferred), ensuring you import apierrors and reference the OperatorConfig fetch in loadBuildConfig to make the distinction clear.
164-172: Stage status always shows "Created" regardless of actual TaskRun state.The
syncStatusFromPipelineRunfunction sets all stages toState: "Created". This doesn't reflect the actual progress of individual tasks (e.g., Running, Succeeded, Failed).Consider fetching individual
TaskRunstatuses to provide more accurate stage progress, or at minimum, derive state from theChildStatusReferencefields if available (e.g., check if the TaskRun has completed).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/controller/softwarebuild/controller.go` around lines 164 - 172, syncStatusFromPipelineRun currently sets every stage to State: "Created" using pr.Status.ChildReferences, which hides real TaskRun progress; change it to derive stage State and Message from each ChildStatusReference/TaskRun status instead. For each cr in pr.Status.ChildReferences (and when present cr.Status or ChildStatusReference fields), inspect completion fields (e.g., TaskRun condition or CompletionTime) to map to "Running", "Succeeded", "Failed", etc., and set automotivev1alpha1.SoftwareBuildStageStatus{Name: cr.PipelineTaskName, State: <derived-state>, Message: <taskrun message or condition>}; if ChildStatusReference lacks details, fetch the referenced TaskRun by name and read its status to populate sb.Status.Stages so stages reflect actual task state instead of always "Created".internal/controller/test/softwarebuild_controller_test.go (1)
83-90: AfterEach assumes resource always exists, which may cause flaky tests.If
BeforeEachfails between theGetcheck andCreate, or if a test modifies the resource state,AfterEachwill fail onExpect(err).NotTo(HaveOccurred()). Consider usingclient.IgnoreNotFound:♻️ Proposed fix
AfterEach(func() { resource := &automotivev1alpha1.SoftwareBuild{} err := k8sClient.Get(ctx, typeNamespacedName, resource) - Expect(err).NotTo(HaveOccurred()) + if err != nil { + return // Resource doesn't exist, nothing to clean up + } By("Cleanup the specific resource instance SoftwareBuild") Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/controller/test/softwarebuild_controller_test.go` around lines 83 - 90, The AfterEach block assumes the SoftwareBuild resource always exists; change the Get/expect logic to tolerate NotFound by using client.IgnoreNotFound on the error returned from k8sClient.Get (or check apierrors.IsNotFound) before asserting success, e.g., call err := k8sClient.Get(ctx, typeNamespacedName, resource) and then Expect(client.IgnoreNotFound(err)).To(Succeed()) so deletion (k8sClient.Delete) runs only when appropriate and the test won't fail if the resource was already absent; update the AfterEach that references resource, k8sClient.Get, typeNamespacedName and k8sClient.Delete accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/common/tasks/software_build.go`:
- Around line 117-124: The git clone command is built by interpolating untrusted
inputs (sb.Spec.Source.Git.Revision and sb.Spec.Source.Git.URL) into a shell
string which can lead to command injection; update the SoftwareBuild logic that
constructs gitClone/fetchCommand so it does not perform shell interpolation —
either validate and strictly whitelist allowed characters for revision and URL
before use, or (preferably) replace the string-concatenated git clone with a
safe invocation that supplies revision and URL as arguments (e.g., use a Tekton
git-clone task or an API/exec that passes args without shell parsing) and ensure
any fallback default for revision ("main") is applied after validation. Ensure
you change the code paths that set the local variables revision, gitClone and
fetchCommand so untrusted values are never directly injected into a shell
command.
- Around line 170-173: The code uses resource.MustParse(pvcSize) inside the
VolumeResourceRequirements Requests which will panic on invalid PVC size;
replace MustParse with resource.ParseQuantity(pvcSize) and handle the returned
(*Quantity, error) — if ParseQuantity returns an error, either return/propagate
that error from the surrounding function (so the controller can fail gracefully)
or fallback to a safe default (e.g., resource.MustParse("1Gi")) and log the bad
value; update the construction of corev1.ResourceList to use the parsed quantity
variable (or the default) instead of calling MustParse directly.
In `@internal/controller/operatorconfig/controller.go`:
- Around line 1028-1032: The call to tasks.GenerateSoftwareBuildPipeline
currently passes nil so OperatorConfig tuning fields are ignored; update the
call in controller.go to construct and pass a pipeline config using the
OperatorConfig values (read from the controller's config object) so that
spec.softwareBuilds.pvcSize, buildTimeoutMinutes, and defaultImage are forwarded
to GenerateSoftwareBuildPipeline instead of nil; locate the call to
tasks.GenerateSoftwareBuildPipeline(Task.SoftwareBuildPipelineName,
config.Namespace, nil) and replace the nil with a struct or map holding the PVC
size, timeout, and image derived from the OperatorConfig (types defined in
api/v1alpha1/operatorconfig_types.go) so GenerateSoftwareBuildPipeline receives
and applies those fields.
- Around line 289-294: The current reconcile swallows errors from
deploySoftwareBuilds and never cleans up when spec.softwareBuilds.enabled is
turned off; modify the controller logic around config.Spec.SoftwareBuilds so
that when SoftwareBuilds is non-nil and Enabled is true you call
r.deploySoftwareBuilds(ctx, config) and propagate any returned error (do not
only log it) so reconciliation retries, and when SoftwareBuilds is nil or
Enabled is false call a new r.cleanupSoftwareBuilds(ctx, config) (implement
cleanup to remove managed pipeline resources) and propagate cleanup errors as
well; ensure the functions referenced (deploySoftwareBuilds,
cleanupSoftwareBuilds, and any callers in controller.reconcile loop) return and
surface errors instead of swallowing them.
In `@internal/controller/softwarebuild/controller_test.go`:
- Around line 24-39: The helper prWithCondition currently ignores its status
parameter and hardcodes the condition Status to "True"; update prWithCondition
to use the provided status parameter for the condition's Status field (e.g.,
replace Status: "True" with Status: status or cast status to the appropriate
ConditionStatus type), and if the helper is not used anywhere in the tests,
either remove prWithCondition or add tests that use it.
---
Nitpick comments:
In `@internal/controller/softwarebuild/controller.go`:
- Around line 62-69: The status update error after detecting an existing
PipelineRun is currently ignored; in createPipelineRun, capture the error
returned by r.Status().Update(ctx, sb) when setting sb.Status.PipelineRunName =
pr.Name and either log it or return it so failures are not silent. Pass a logger
into createPipelineRun (or make logger accessible) and replace the blank
assignment "_ = r.Status().Update(...)" with proper error handling: check the
returned error from r.Status().Update, call logger.Error/Infof with context
including sb.Name and the error (or return a wrapped error) to ensure the update
failure is visible and reconciliation can react accordingly.
- Around line 179-191: The loadBuildConfig function currently swallows any error
from r.Get; change it to distinguish NotFound from other errors by using
k8s.io/apimachinery/pkg/api/errors (apierrors.IsNotFound): if the get returns
NotFound, return nil as before; if it returns any other error, log the error via
the reconciler logger (e.g., r.Log.Error(err, "failed to get OperatorConfig",
"name", "default", "namespace", namespace)) and return nil (or propagate the
error from the caller if preferred), ensuring you import apierrors and reference
the OperatorConfig fetch in loadBuildConfig to make the distinction clear.
- Around line 164-172: syncStatusFromPipelineRun currently sets every stage to
State: "Created" using pr.Status.ChildReferences, which hides real TaskRun
progress; change it to derive stage State and Message from each
ChildStatusReference/TaskRun status instead. For each cr in
pr.Status.ChildReferences (and when present cr.Status or ChildStatusReference
fields), inspect completion fields (e.g., TaskRun condition or CompletionTime)
to map to "Running", "Succeeded", "Failed", etc., and set
automotivev1alpha1.SoftwareBuildStageStatus{Name: cr.PipelineTaskName, State:
<derived-state>, Message: <taskrun message or condition>}; if
ChildStatusReference lacks details, fetch the referenced TaskRun by name and
read its status to populate sb.Status.Stages so stages reflect actual task state
instead of always "Created".
In `@internal/controller/test/softwarebuild_controller_test.go`:
- Around line 83-90: The AfterEach block assumes the SoftwareBuild resource
always exists; change the Get/expect logic to tolerate NotFound by using
client.IgnoreNotFound on the error returned from k8sClient.Get (or check
apierrors.IsNotFound) before asserting success, e.g., call err :=
k8sClient.Get(ctx, typeNamespacedName, resource) and then
Expect(client.IgnoreNotFound(err)).To(Succeed()) so deletion (k8sClient.Delete)
runs only when appropriate and the test won't fail if the resource was already
absent; update the AfterEach that references resource, k8sClient.Get,
typeNamespacedName and k8sClient.Delete accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 27ca7ee8-dbe7-47f8-9a0b-5d91514fecca
⛔ Files ignored due to path filters (3)
config/crd/bases/automotive.sdv.cloud.redhat.com_operatorconfigs.yamlis excluded by!config/crd/bases/**config/crd/bases/automotive.sdv.cloud.redhat.com_softwarebuilds.yamlis excluded by!config/crd/bases/**config/rbac/role.yamlis excluded by!config/rbac/role.yaml
📒 Files selected for processing (16)
PROJECTREADME.mdapi/v1alpha1/operatorconfig_types.goapi/v1alpha1/softwarebuild_types.goapi/v1alpha1/softwarebuild_types_test.goapi/v1alpha1/zz_generated.deepcopy.gocmd/main.goconfig/crd/kustomization.yamlconfig/samples/automotive_v1alpha1_softwarebuild.yamlconfig/samples/automotive_v1alpha1_softwarebuild_zephyr.yamlinternal/common/tasks/software_build.gointernal/common/tasks/software_build_test.gointernal/controller/operatorconfig/controller.gointernal/controller/softwarebuild/controller.gointernal/controller/softwarebuild/controller_test.gointernal/controller/test/softwarebuild_controller_test.go
| func softwareBuildStageTask(stageName, paramImage, paramCommand string) tektonv1.PipelineTask { | ||
| return tektonv1.PipelineTask{ | ||
| Name: stageName, | ||
| TaskSpec: &tektonv1.EmbeddedTask{ | ||
| TaskSpec: tektonv1.TaskSpec{ | ||
| Params: []tektonv1.ParamSpec{ | ||
| {Name: "image", Type: tektonv1.ParamTypeString}, | ||
| {Name: "command", Type: tektonv1.ParamTypeString}, | ||
| }, | ||
| Workspaces: []tektonv1.WorkspaceDeclaration{ | ||
| {Name: "ws", MountPath: "/workspace"}, | ||
| }, | ||
| Steps: []tektonv1.Step{ | ||
| { | ||
| Name: "run", | ||
| Image: "$(params.image)", | ||
| ImagePullPolicy: corev1.PullIfNotPresent, | ||
| Script: `#!/usr/bin/env bash | ||
| set -euo pipefail | ||
| cd $(workspaces.ws.path) | ||
| bash -lc "$(params.command)" | ||
| `, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| Params: []tektonv1.Param{ | ||
| {Name: "image", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: paramImage}}, | ||
| {Name: "command", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: paramCommand}}, | ||
| }, | ||
| Workspaces: []tektonv1.WorkspacePipelineTaskBinding{ | ||
| {Name: "ws", Workspace: "shared-workspace"}, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // GenerateSoftwareBuildPipeline creates the Tekton Pipeline that runs five | ||
| // sequential stages inside a user-chosen container image. | ||
| func GenerateSoftwareBuildPipeline(name, namespace string, _ *BuildConfig) *tektonv1.Pipeline { | ||
| stages := []string{"fetch", "prebuild", "build", "postbuild", "deploy"} | ||
|
|
||
| tasks := make([]tektonv1.PipelineTask, len(stages)) | ||
| for i, s := range stages { | ||
| tasks[i] = softwareBuildStageTask( | ||
| s, | ||
| "$(params.containerImage)", | ||
| fmt.Sprintf("$(params.%sCommand)", s), | ||
| ) | ||
| if i > 0 { | ||
| tasks[i].RunAfter = []string{stages[i-1]} | ||
| } | ||
| } | ||
|
|
||
| return &tektonv1.Pipeline{ | ||
| TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "Pipeline"}, | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: name, | ||
| Namespace: namespace, | ||
| Labels: map[string]string{ | ||
| "app.kubernetes.io/managed-by": "automotive-dev-operator", | ||
| }, | ||
| }, | ||
| Spec: tektonv1.PipelineSpec{ | ||
| Params: []tektonv1.ParamSpec{ | ||
| { | ||
| Name: "containerImage", Type: tektonv1.ParamTypeString, | ||
| Default: &tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: defaultSoftwareBuildImage}, | ||
| Description: "Container image providing the build toolchain", | ||
| }, | ||
| {Name: "fetchCommand", Type: tektonv1.ParamTypeString, Description: "Fetch stage command"}, | ||
| {Name: "prebuildCommand", Type: tektonv1.ParamTypeString, Description: "Prebuild stage command"}, | ||
| {Name: "buildCommand", Type: tektonv1.ParamTypeString, Description: "Build stage command"}, | ||
| {Name: "postbuildCommand", Type: tektonv1.ParamTypeString, Description: "Postbuild stage command"}, | ||
| {Name: "deployCommand", Type: tektonv1.ParamTypeString, Description: "Deploy stage command"}, | ||
| }, | ||
| Workspaces: []tektonv1.PipelineWorkspaceDeclaration{ | ||
| {Name: "shared-workspace"}, | ||
| }, | ||
| Tasks: tasks, | ||
| }, | ||
| } | ||
| } |
There was a problem hiding this comment.
[CRITICAL] The pipeline uses inline TaskSpec in PipelineTask definitions instead of TaskRef pointing to versioned, signed task bundles. Inline task specs cannot be verified by Tekton Chains' signature verification, so SLSA provenance attestation is effectively impossible here. The tasks also produce no typed results (e.g., IMAGE_URL, IMAGE_DIGEST), and bash -lc "$(params.command)" allows arbitrary unauditable command execution.
Suggestion: Switch to TaskRef pointing to versioned, signed task bundles. Add result parameters for build outputs (artifact hashes, image digests) so Tekton Chains can generate provenance attestations.
There was a problem hiding this comment.
Acknowledged — this is a valid concern for production supply-chain security.
For this initial iteration, inline TaskSpec is intentional: it keeps the implementation self-contained without requiring users to set up and maintain a separate OCI bundle registry for task images. The operator generates the pipeline dynamically from the CRD spec, and inline tasks are the natural fit for that pattern.
That said, I agree that for SLSA provenance and Tekton Chains integration, we'll need to move to TaskRef with versioned, signed bundles. I've created #200 to track this:
- Phase 1 (this PR): Inline TaskSpec — functional, self-contained, no external dependencies
- Phase 2 (Migrate SoftwareBuild pipeline from inline TaskSpec to signed TaskRef bundles #200): Extract tasks into signed OCI bundles, switch to
TaskRef, add typed results (IMAGE_URL,IMAGE_DIGEST) for Chains attestation
The bash -lc "$(params.command)" pattern is consistent with how the existing ImageBuild pipeline works in this operator (it also executes user-provided commands). We can add command allowlisting or policy enforcement as part of #200.
Would you be OK with this phased approach?
There was a problem hiding this comment.
Acknowledged — this is tracked in #200, which will leverage the TSSF integration work (see ADR docs/adr/001-tssf-integration.md). Once Tekton Chains + Enterprise Contract are wired up on main, SoftwareBuild will migrate from inline TaskSpec to signed TaskRef bundles with typed result parameters (IMAGE_URL, IMAGE_DIGEST, etc.), matching the pattern established by #197. The inline approach here is intentionally a starting point.
| gitClone := fmt.Sprintf("git clone --branch %s --single-branch %s src\n", revision, sb.Spec.Source.Git.URL) | ||
| fetchCommand = gitClone + fetchCommand |
There was a problem hiding this comment.
[CRITICAL] Command injection vulnerability. fmt.Sprintf("git clone --branch %s --single-branch %s src\n", revision, sb.Spec.Source.Git.URL) directly interpolates user-provided values into a shell command without any quoting or escaping. The URL validation pattern (^https?://|^git@) only checks the prefix, so a crafted URL like https://evil.com/repo$(curl attacker.com) or a revision like main; rm -rf / would execute arbitrary commands.
Suggestion: Quote all interpolated values and use -- to separate options from arguments: fmt.Sprintf("git clone --branch '%s' --single-branch -- '%s' src\n", revision, url). Also add a CRD validation pattern for the revision field.
There was a problem hiding this comment.
Agreed — this is a real vulnerability. Will fix by shell-quoting all interpolated values and tightening the URL validation regex.
| fetchCommand = gitClone + fetchCommand | ||
| } | ||
|
|
||
| prName := fmt.Sprintf("%s-%d", sb.Name, time.Now().Unix()) |
There was a problem hiding this comment.
[HIGH] Non-deterministic PipelineRun naming breaks idempotency. fmt.Sprintf("%s-%d", sb.Name, time.Now().Unix()) generates a new name on each reconciliation. If the status update at controller.go:81-83 fails after PipelineRun creation, the next reconciliation creates another PipelineRun with a different name, and the AlreadyExists check won't catch it because the name differs. This creates orphaned PipelineRuns.
Suggestion: Use a deterministic name, e.g., fmt.Sprintf("%s-gen%d", sb.Name, sb.Generation), or persist the intended name in status before creating the PipelineRun.
There was a problem hiding this comment.
Agreed. Will switch to a deterministic name using sb.Generation, e.g. fmt.Sprintf("%s-gen%d", sb.Name, sb.Generation). This eliminates orphaned PipelineRuns on status update failures.
| // Deploy software build pipeline when enabled | ||
| if config.Spec.SoftwareBuilds != nil && config.Spec.SoftwareBuilds.Enabled { | ||
| if err := r.deploySoftwareBuilds(ctx, config); err != nil { | ||
| log.Error(err, "Failed to deploy SoftwareBuilds pipeline") | ||
| } | ||
| } |
There was a problem hiding this comment.
[HIGH] Disabling softwareBuilds in OperatorConfig does not clean up the pipeline. The reconciler deploys the software-build-pipeline when softwareBuilds.enabled is true but has no cleanup path when disabled. There is no cleanupSoftwareBuilds function.
Suggestion: Add a cleanupSoftwareBuilds method that deletes the software-build-pipeline Pipeline, and call it when softwareBuilds is nil or enabled is false.
There was a problem hiding this comment.
Agreed. Will add a cleanupSoftwareBuilds method that deletes the software-build-pipeline Pipeline, and call it when softwareBuilds is nil or enabled is false.
| func GenerateSoftwareBuildPipelineRun(sb *automotivev1alpha1.SoftwareBuild, config *BuildConfig) *tektonv1.PipelineRun { | ||
| image := sb.Spec.Runtime.Image | ||
| if image == "" { | ||
| image = defaultSoftwareBuildImage | ||
| } | ||
|
|
||
| pvcSize := softwareBuildPVCSize | ||
| if config != nil && config.PVCSize != "" { | ||
| pvcSize = config.PVCSize | ||
| } | ||
|
|
||
| fetchCommand := sb.Spec.Stages.Fetch.Command | ||
| if sb.Spec.Source.Type == automotivev1alpha1.SoftwareBuildSourceGit && sb.Spec.Source.Git != nil { | ||
| revision := sb.Spec.Source.Git.Revision | ||
| if revision == "" { | ||
| revision = "main" | ||
| } | ||
| gitClone := fmt.Sprintf("git clone --branch %s --single-branch %s src\n", revision, sb.Spec.Source.Git.URL) | ||
| fetchCommand = gitClone + fetchCommand | ||
| } | ||
|
|
||
| prName := fmt.Sprintf("%s-%d", sb.Name, time.Now().Unix()) | ||
|
|
||
| return &tektonv1.PipelineRun{ | ||
| TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "PipelineRun"}, | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: prName, | ||
| Namespace: sb.Namespace, | ||
| Labels: map[string]string{ | ||
| "automotive.sdv.cloud.redhat.com/softwarebuild": sb.Name, | ||
| "app.kubernetes.io/managed-by": "automotive-dev-operator", | ||
| }, | ||
| }, | ||
| Spec: tektonv1.PipelineRunSpec{ | ||
| PipelineRef: &tektonv1.PipelineRef{Name: SoftwareBuildPipelineName}, | ||
| Params: []tektonv1.Param{ | ||
| {Name: "containerImage", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: image}}, | ||
| {Name: "fetchCommand", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: fetchCommand}}, | ||
| {Name: "prebuildCommand", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: sb.Spec.Stages.Prebuild.Command}}, | ||
| {Name: "buildCommand", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: sb.Spec.Stages.Build.Command}}, | ||
| {Name: "postbuildCommand", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: sb.Spec.Stages.Postbuild.Command}}, | ||
| {Name: "deployCommand", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: sb.Spec.Stages.Deploy.Command}}, | ||
| }, | ||
| Workspaces: buildWorkspaceBinding(sb, pvcSize), | ||
| }, | ||
| } | ||
| } |
There was a problem hiding this comment.
[HIGH] Timeout from SoftwareBuild spec is not propagated to PipelineRun. SoftwareBuildSpec.TimeoutSeconds is declared in the API but never used in GenerateSoftwareBuildPipelineRun. The PipelineRun has no Spec.Timeouts set, and BuildConfig.BuildTimeoutMinutes is also loaded but never applied.
Suggestion: Set pr.Spec.Timeouts when TimeoutSeconds > 0, falling back to config.BuildTimeoutMinutes.
There was a problem hiding this comment.
Agreed. Will set pr.Spec.Timeouts from TimeoutSeconds when > 0, falling back to config.BuildTimeoutMinutes.
| func buildWorkspaceBinding(sb *automotivev1alpha1.SoftwareBuild, pvcSize string) []tektonv1.WorkspaceBinding { | ||
| if sb.Spec.Source.Type == automotivev1alpha1.SoftwareBuildSourcePVC && sb.Spec.Source.PVC != nil { | ||
| return []tektonv1.WorkspaceBinding{ | ||
| { | ||
| Name: "shared-workspace", | ||
| PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ | ||
| ClaimName: sb.Spec.Source.PVC.ClaimName, | ||
| }, | ||
| }, | ||
| } | ||
| } |
There was a problem hiding this comment.
[MEDIUM] No test for PVC source workspace binding. buildWorkspaceBinding has a PVC branch that is not tested. Only the VolumeClaimTemplate default path is covered.
Suggestion: Add a test with Source.Type = "pvc" verifying the workspace uses PersistentVolumeClaim binding.
There was a problem hiding this comment.
Agreed. Tracked in #203 — will add a test with Source.Type = "pvc" verifying the PVC workspace binding.
There was a problem hiding this comment.
i think you addressed this in one of your latest commits. seems fine for now.
| func TestSoftwareBuildPhaseConstants(t *testing.T) { | ||
| phases := []SoftwareBuildPhase{ | ||
| SoftwareBuildPhasePending, | ||
| SoftwareBuildPhaseRunning, | ||
| SoftwareBuildPhaseSucceeded, | ||
| SoftwareBuildPhaseFailed, | ||
| } | ||
| expected := []string{"Pending", "Running", "Succeeded", "Failed"} | ||
| for i, p := range phases { | ||
| if string(p) != expected[i] { | ||
| t.Errorf("phase %d: got %q, want %q", i, p, expected[i]) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestSoftwareBuildSourceTypeConstants(t *testing.T) { | ||
| if SoftwareBuildSourceGit != "git" { | ||
| t.Errorf("expected git, got %q", SoftwareBuildSourceGit) | ||
| } | ||
| if SoftwareBuildSourcePVC != "pvc" { | ||
| t.Errorf("expected pvc, got %q", SoftwareBuildSourcePVC) | ||
| } | ||
| if SoftwareBuildSourceHostPath != "hostPath" { | ||
| t.Errorf("expected hostPath, got %q", SoftwareBuildSourceHostPath) | ||
| } | ||
| } |
There was a problem hiding this comment.
[MEDIUM] Tests verify constant values rather than behavior. TestSoftwareBuildPhaseConstants and TestSoftwareBuildSourceTypeConstants only verify that constants equal expected strings. These cannot fail unless the constants themselves are changed, providing minimal value.
Suggestion: Replace with behavioral tests, such as serialization round-trips or CRD validation behavior.
There was a problem hiding this comment.
Partially agreed. The constant tests do catch accidental renames, but higher-value behavioral tests are needed. Tracked in #203.
| func (r *SoftwareBuildReconciler) syncStatusFromPipelineRun(sb *automotivev1alpha1.SoftwareBuild, pr *tektonv1.PipelineRun) { | ||
| phase := automotivev1alpha1.SoftwareBuildPhaseRunning | ||
| condStatus := metav1.ConditionFalse | ||
| reason := "Running" | ||
| message := "PipelineRun is in progress" | ||
|
|
||
| for _, c := range pr.Status.Conditions { | ||
| if c.Type == "Succeeded" { | ||
| switch c.Status { | ||
| case "True": | ||
| phase = automotivev1alpha1.SoftwareBuildPhaseSucceeded | ||
| condStatus = metav1.ConditionTrue | ||
| reason = string(c.Reason) | ||
| message = c.Message | ||
| case "False": | ||
| phase = automotivev1alpha1.SoftwareBuildPhaseFailed | ||
| condStatus = metav1.ConditionFalse | ||
| reason = string(c.Reason) | ||
| message = c.Message | ||
| sb.Status.FailureReason = string(c.Reason) | ||
| default: | ||
| phase = automotivev1alpha1.SoftwareBuildPhaseRunning | ||
| } | ||
| } | ||
| } | ||
|
|
||
| sb.Status.Phase = phase | ||
| meta.SetStatusCondition(&sb.Status.Conditions, metav1.Condition{ | ||
| Type: conditionReady, | ||
| Status: condStatus, | ||
| Reason: reason, | ||
| Message: message, | ||
| ObservedGeneration: sb.Generation, | ||
| }) | ||
|
|
||
| stages := make([]automotivev1alpha1.SoftwareBuildStageStatus, 0, len(pr.Status.ChildReferences)) | ||
| for _, cr := range pr.Status.ChildReferences { | ||
| stages = append(stages, automotivev1alpha1.SoftwareBuildStageStatus{ | ||
| Name: cr.PipelineTaskName, | ||
| State: "Created", | ||
| Message: fmt.Sprintf("TaskRun: %s", cr.Name), | ||
| }) | ||
| } | ||
| sb.Status.Stages = stages | ||
|
|
||
| if sb.Spec.Destination.Path != "" { | ||
| sb.Status.ArtifactURI = sb.Spec.Destination.Path | ||
| } | ||
| } |
There was a problem hiding this comment.
[MEDIUM] syncStatusFromPipelineRun handles three concerns: phase mapping, condition setting, and stage status building. This makes it harder to test and maintain.
Suggestion: Extract into separate functions: mapPipelineRunPhase, buildStageStatuses, and handle artifact URI separately.
There was a problem hiding this comment.
Agreed. Will extract into mapPipelineRunPhase and buildStageStatuses for better testability.
| // Image is the container image that provides the build toolchain. | ||
| // +kubebuilder:default="ubuntu:24.04" |
There was a problem hiding this comment.
[MEDIUM] Container image tags are mutable, undermining build reproducibility. Runtime.Image accepts mutable tags like ubuntu:24.04. The same tag can point to different images over time, so two builds from the same SoftwareBuild spec can produce different results.
Suggestion: Resolve tags to digests at reconciliation time and record the resolved digest in status for reproducibility.
There was a problem hiding this comment.
Partially agreed. For v1, will record the resolved image digest in status at reconciliation time for reproducibility. Enforcing digest-only inputs would be too restrictive for the UX. Full image digest resolution is tracked in #200.
There was a problem hiding this comment.
Tracking it does not resolve the problem. I would like to see this addressed before merging but am also curious about @bennyz thoughts on this. I think we have workspaces to run arbitrary commands - pipelines should be deterministic.
There was a problem hiding this comment.
Again, this code is not final! It's the first implementation. I reiterate that this will be improved with #200 (and #197).
long version:
Arbitrary shell commands are a deliberate design choice, not an oversight. SoftwareBuild targets a different audience than ImageBuild — embedded developers who bring their own toolchains (Zephyr's west, Yocto's bitbake, plain make, etc.). We cannot enumerate every build system into a typed API.
"Pipelines should be deterministic" applies at the CI/CD level, where the commands are checked into source control (the SoftwareBuild CR YAML). The CRD doesn't make them less deterministic than a Tekton Pipeline with inline scripts — it structures them into well-known stages with per-stage image override, timeout, and lifecycle tracking.
Workspaces serve a different purpose: interactive development environments. A build pipeline needs reproducible, ephemeral execution with artifact outputs — which is what SoftwareBuild provides.
That said, when Tekton Chains integration lands (#200), command execution will happen inside signed task bundles with SLSA provenance, giving us auditability even for arbitrary commands.
| // +optional | ||
| ServiceAccountName string `json:"serviceAccountName,omitempty"` |
There was a problem hiding this comment.
[LOW] ServiceAccountName is declared in the API but not wired to PipelineRun. SoftwareBuildRuntimeSpec.ServiceAccountName exists but GenerateSoftwareBuildPipelineRun never sets Spec.TaskRunTemplate.ServiceAccountName.
Suggestion: Wire the field when non-empty so that builds can run under a user-specified service account.
There was a problem hiding this comment.
Agreed. Will wire ServiceAccountName to PipelineRun.Spec.TaskRunTemplate.ServiceAccountName when non-empty.
There was a problem hiding this comment.
The original issue seems resolved but ServiceAccountName now allows arbitrary SA reference without validation or documentation.
The optional ServiceAccountName field is propagated directly to the PipelineRun's TaskRunTemplate.ServiceAccountName with no validation. A user with SoftwareBuild RBAC can reference a high-privilege SA, and the build container runs with that SA's token. This is inherent to any CRD that creates pods/PipelineRuns.
Suggestion: document that granting SoftwareBuild creation RBAC is equivalent to granting pod creation rights within the target namespace and an allowlist in OperatorConfig.
There was a problem hiding this comment.
I suggest tracking this as a bug of documentation and this will get fixed as soon as this lands into main.
There was a problem hiding this comment.
Good point. This is inherent to any CRD that creates pods on behalf of users — granting SoftwareBuild RBAC is equivalent to granting pod creation rights in the namespace. We can document this in the CRD's godoc and add an allowlist in OperatorConfig as a follow-up. The current behavior is identical to how ImageBuild handles SA references.
Addresses centos-automotive-suite#203: - Fix prWithCondition helper to use its status parameter (was hardcoded to "True", hiding test intent) - Replace literal constant-value tests with JSON round-trip tests that verify serialization fidelity for Git, PVC, and status types - Add PVC workspace binding test (source.type=pvc binds existing PVC instead of VolumeClaimTemplate) - Add git source clone prepend test (verifies URL and revision injection) - Add git source default revision test (defaults to "main") - Add ImagePullPolicy test (all pipeline steps use IfNotPresent) - Add failed condition Ready=False test - Add ObservedGeneration tracking test - Add multi-stage ChildReferences population test Closes centos-automotive-suite#203 Made-with: Cursor
There was a problem hiding this comment.
🧹 Nitpick comments (2)
api/v1alpha1/softwarebuild_types_test.go (2)
64-100: Add assertions for all populated PVC-source fields.
Pathis set in the fixture but never asserted, so regressions inSoftwareBuildPVCSource.Pathserialization would pass unnoticed.✅ Suggested assertion additions
if roundTripped.Spec.Source.PVC.ClaimName != "my-pvc" { t.Errorf("claimName: got %q, want my-pvc", roundTripped.Spec.Source.PVC.ClaimName) } + if roundTripped.Spec.Source.PVC.Path != "/data" { + t.Errorf("path: got %q, want /data", roundTripped.Spec.Source.PVC.Path) + } if roundTripped.Spec.Source.Git != nil { t.Error("git source should be nil for PVC source type") } + if roundTripped.Spec.Source.HostPath != nil { + t.Error("hostPath source should be nil for PVC source type") + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/v1alpha1/softwarebuild_types_test.go` around lines 64 - 100, The test TestSoftwareBuildSpec_PVCSource_JSONRoundTrip is missing an assertion for the populated SoftwareBuildPVCSource.Path field; after checking roundTripped.Spec.Source.PVC != nil and ClaimName, add an assertion that roundTripped.Spec.Source.PVC.Path equals "/data" (and keep the existing checks for Source.Type and Git == nil) to ensure Path serialization/deserialization is validated.
102-135: AssertArtifactURIand stage state to complete status coverage.The fixture sets
ArtifactURIandState, but current checks skip them.✅ Suggested assertion additions
if roundTripped.PipelineRunName != "build-gen1" { t.Errorf("pipelineRunName: got %q", roundTripped.PipelineRunName) } + if roundTripped.ArtifactURI != "/workspace/artifacts" { + t.Errorf("artifactURI: got %q, want /workspace/artifacts", roundTripped.ArtifactURI) + } if len(roundTripped.Stages) != 2 { t.Fatalf("stages: got %d, want 2", len(roundTripped.Stages)) } if roundTripped.Stages[1].Name != "build" { t.Errorf("stage[1] name: got %q, want build", roundTripped.Stages[1].Name) } + if roundTripped.Stages[1].State != "Completed" { + t.Errorf("stage[1] state: got %q, want Completed", roundTripped.Stages[1].State) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/v1alpha1/softwarebuild_types_test.go` around lines 102 - 135, In TestSoftwareBuildStatus_JSONRoundTrip, add assertions to verify ArtifactURI and each stage's State on the roundTripped value: assert roundTripped.ArtifactURI == "/workspace/artifacts" and that roundTripped.Stages[0].State and roundTripped.Stages[1].State equal "Completed" (use t.Errorf/t.Fatalf similar to the existing checks); this ensures the JSON marshal/unmarshal preserved SoftwareBuildStatus.ArtifactURI and SoftwareBuildStageStatus.State for the SoftwareBuildStatus/SoftwareBuildStageStatus types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@api/v1alpha1/softwarebuild_types_test.go`:
- Around line 64-100: The test TestSoftwareBuildSpec_PVCSource_JSONRoundTrip is
missing an assertion for the populated SoftwareBuildPVCSource.Path field; after
checking roundTripped.Spec.Source.PVC != nil and ClaimName, add an assertion
that roundTripped.Spec.Source.PVC.Path equals "/data" (and keep the existing
checks for Source.Type and Git == nil) to ensure Path
serialization/deserialization is validated.
- Around line 102-135: In TestSoftwareBuildStatus_JSONRoundTrip, add assertions
to verify ArtifactURI and each stage's State on the roundTripped value: assert
roundTripped.ArtifactURI == "/workspace/artifacts" and that
roundTripped.Stages[0].State and roundTripped.Stages[1].State equal "Completed"
(use t.Errorf/t.Fatalf similar to the existing checks); this ensures the JSON
marshal/unmarshal preserved SoftwareBuildStatus.ArtifactURI and
SoftwareBuildStageStatus.State for the
SoftwareBuildStatus/SoftwareBuildStageStatus types.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5ca889c7-fc92-4eb5-864d-b3797fd1e98d
📒 Files selected for processing (3)
api/v1alpha1/softwarebuild_types_test.gointernal/common/tasks/software_build_test.gointernal/controller/softwarebuild/controller_test.go
✅ Files skipped from review due to trivial changes (1)
- internal/common/tasks/software_build_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- internal/controller/softwarebuild/controller_test.go
CRD changes (softwarebuild_types.go): - Remove hostPath source type (not implemented) (centos-automotive-suite#7) - Add CEL validation for source type/field consistency (centos-automotive-suite#11) - Remove unused destination types (registry, artifactory, quay) (centos-automotive-suite#16) - Tighten Git URL and revision validation patterns (centos-automotive-suite#1) Pipeline generation (software_build.go): - Fix command injection: quote URL and revision in git clone (centos-automotive-suite#1) - Sanitize revision with regex, fall back to "main" on invalid input - Deterministic PipelineRun naming using sb.Generation (centos-automotive-suite#3) - Propagate TimeoutSeconds and BuildTimeoutMinutes to PipelineRun (centos-automotive-suite#6) - Add SecurityContext with AllowPrivilegeEscalation=false (centos-automotive-suite#12) - Wire per-stage image override (centos-automotive-suite#17) - Use ParseQuantity with fallback instead of MustParse (centos-automotive-suite#26) - Wire ServiceAccountName to PipelineRun (centos-automotive-suite#28) - Wire OperatorConfig DefaultImage through BuildConfig (centos-automotive-suite#14) Controller (softwarebuild/controller.go): - Handle status update errors instead of discarding them (centos-automotive-suite#8) - Distinguish Pending from Running using StartTime (centos-automotive-suite#18) - Add error context wrapping on all return paths (centos-automotive-suite#19) - Replace magic strings with typed constants (centos-automotive-suite#20) - Extract mapPipelineRunPhase and buildStageStatuses helpers (centos-automotive-suite#24) OperatorConfig controller: - Propagate deploySoftwareBuilds error and set Failed status (centos-automotive-suite#4) - Add cleanupSoftwareBuilds for when feature is disabled (centos-automotive-suite#5) - Wire SoftwareBuildsConfig fields to pipeline generation (centos-automotive-suite#14) Made-with: Cursor
CRD changes (softwarebuild_types.go): - Remove hostPath source type (not implemented) (centos-automotive-suite#7) - Add CEL validation for source type/field consistency (centos-automotive-suite#11) - Remove unused destination types (registry, artifactory, quay) (centos-automotive-suite#16) - Tighten Git URL and revision validation patterns (centos-automotive-suite#1) Pipeline generation (software_build.go): - Fix command injection: quote URL and revision in git clone (centos-automotive-suite#1) - Sanitize revision with regex, fall back to "main" on invalid input - Deterministic PipelineRun naming using sb.Generation (centos-automotive-suite#3) - Propagate TimeoutSeconds and BuildTimeoutMinutes to PipelineRun (centos-automotive-suite#6) - Add SecurityContext with AllowPrivilegeEscalation=false (centos-automotive-suite#12) - Wire per-stage image override (centos-automotive-suite#17) - Use ParseQuantity with fallback instead of MustParse (centos-automotive-suite#26) - Wire ServiceAccountName to PipelineRun (centos-automotive-suite#28) - Wire OperatorConfig DefaultImage through BuildConfig (centos-automotive-suite#14) Controller (softwarebuild/controller.go): - Handle status update errors instead of discarding them (centos-automotive-suite#8) - Distinguish Pending from Running using StartTime (centos-automotive-suite#18) - Add error context wrapping on all return paths (centos-automotive-suite#19) - Replace magic strings with typed constants (centos-automotive-suite#20) - Extract mapPipelineRunPhase and buildStageStatuses helpers (centos-automotive-suite#24) OperatorConfig controller: - Propagate deploySoftwareBuilds error and set Failed status (centos-automotive-suite#4) - Add cleanupSoftwareBuilds for when feature is disabled (centos-automotive-suite#5) - Wire SoftwareBuildsConfig fields to pipeline generation (centos-automotive-suite#14) Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
api/v1alpha1/softwarebuild_types.go (1)
111-118: Stages fields are all required — consider adding+optionalmarkers as agreed.Per past review discussion, the fixed stages were to have
+optionalmarkers added so users can omit stages. Currently all five stages (Fetch,Prebuild,Build,Postbuild,Deploy) are required fields.♻️ Proposed fix to make stages optional
// SoftwareBuildPipelineStages groups the five sequential stages. type SoftwareBuildPipelineStages struct { - Fetch SoftwareBuildStageSpec `json:"fetch"` - Prebuild SoftwareBuildStageSpec `json:"prebuild"` - Build SoftwareBuildStageSpec `json:"build"` - Postbuild SoftwareBuildStageSpec `json:"postbuild"` - Deploy SoftwareBuildStageSpec `json:"deploy"` + // +optional + Fetch SoftwareBuildStageSpec `json:"fetch,omitempty"` + // +optional + Prebuild SoftwareBuildStageSpec `json:"prebuild,omitempty"` + // +optional + Build SoftwareBuildStageSpec `json:"build,omitempty"` + // +optional + Postbuild SoftwareBuildStageSpec `json:"postbuild,omitempty"` + // +optional + Deploy SoftwareBuildStageSpec `json:"deploy,omitempty"` }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/v1alpha1/softwarebuild_types.go` around lines 111 - 118, The SoftwareBuildPipelineStages struct currently has all five fields required; add Kubernetes optional markers by placing a `// +optional` comment above each field (Fetch, Prebuild, Build, Postbuild, Deploy) in the SoftwareBuildPipelineStages definition so these stages can be omitted by users; ensure the json tags remain unchanged (e.g., `json:"fetch"`) and keep the type as SoftwareBuildStageSpec unless you also intend to convert fields to pointer types—only the `// +optional` comments are required here.internal/common/tasks/software_build_test.go (1)
344-367: Consider adding a test for URL with embedded single quotes.The unsafe revision test is good, but there's no coverage for malicious URLs containing single quotes (e.g.,
https://evil.com/'$(cmd)'), which is a related injection vector.🧪 Suggested test case
func TestGenerateSoftwareBuildPipelineRun_URLWithQuotesSanitized(t *testing.T) { sb := newTestSoftwareBuild() sb.Spec.Source = automotivev1alpha1.SoftwareBuildSourceSpec{ Type: automotivev1alpha1.SoftwareBuildSourceGit, Git: &automotivev1alpha1.SoftwareBuildGitSource{ URL: "https://evil.com/'$(whoami)'", Revision: "main", }, } pr := GenerateSoftwareBuildPipelineRun(sb, nil) for _, p := range pr.Spec.Params { if p.Name == "fetchCommand" { // Should either reject the URL or properly escape the quotes if strings.Contains(p.Value.StringVal, "$(whoami)") { t.Errorf("URL with embedded command should be sanitized, got %q", p.Value.StringVal) } return } } t.Fatal("fetchCommand param not found") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/common/tasks/software_build_test.go` around lines 344 - 367, Add a new unit test that mirrors TestGenerateSoftwareBuildPipelineRun_UnsafeRevisionSanitized but targets malicious URLs with embedded single quotes (e.g., "https://evil.com/'$(whoami)'"); in the test (e.g., TestGenerateSoftwareBuildPipelineRun_URLWithQuotesSanitized) construct a SoftwareBuild with that Git URL and call GenerateSoftwareBuildPipelineRun, find the "fetchCommand" param in pr.Spec.Params, and assert that the produced p.Value.StringVal neither contains the embedded command substitution ($(whoami)) nor unescaped single-quote characters—this verifies URL inputs are rejected or properly escaped by the code that builds the fetch command in GenerateSoftwareBuildPipelineRun.internal/controller/softwarebuild/controller.go (1)
189-202:loadBuildConfigsilently ignores errors.When the OperatorConfig fetch fails for reasons other than NotFound (e.g., RBAC issues, API server unavailability), returning
nilsilently proceeds with defaults. This could mask configuration problems.♻️ Proposed improvement
func (r *SoftwareBuildReconciler) loadBuildConfig(ctx context.Context, namespace string) *tasks.BuildConfig { + logger := r.Log.WithValues("namespace", namespace) var opConfig automotivev1alpha1.OperatorConfig if err := r.Get(ctx, types.NamespacedName{Name: "default", Namespace: namespace}, &opConfig); err != nil { + if !errors.IsNotFound(err) { + logger.Error(err, "failed to fetch OperatorConfig, using defaults") + } return nil }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/controller/softwarebuild/controller.go` around lines 189 - 202, loadBuildConfig currently swallows all Get errors; change it to distinguish NotFound from other errors and log/report non-NotFound failures. In loadBuildConfig (SoftwareBuildReconciler) import k8s.io/apimachinery/pkg/api/errors as apierrors, then replace the single if err != nil { return nil } with: if apierrors.IsNotFound(err) { return nil } else { r.Log.Error(err, "failed to get OperatorConfig", "namespace", namespace) ; return nil } so that RBAC/API errors are surfaced in logs (optionally consider returning an error from loadBuildConfig later for caller handling); keep the rest of the function returning &tasks.BuildConfig when opConfig present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api/v1alpha1/softwarebuild_types.go`:
- Around line 135-137: The TimeoutSeconds field allows negative values which
leads to invalid metav1.Duration; add a Kubernetes validation constraint to
enforce non-negative values by annotating the TimeoutSeconds field (e.g., add a
kubebuilder validation tag like +kubebuilder:validation:Minimum=0 to the
TimeoutSeconds int64 field) and regenerate CRDs so the API server rejects
negative timeouts; ensure you update any relevant struct comments and run
controller-gen to refresh generated CRD manifests.
In `@internal/common/tasks/software_build.go`:
- Line 192: The git clone command construction in gitClone uses
sb.Spec.Source.Git.URL directly and is vulnerable to single-quote injection
(e.g., in function building gitClone string with revision and
sb.Spec.Source.Git.URL). Before interpolating the URL, escape or sanitize any
single-quote characters in sb.Spec.Source.Git.URL (replace each ' with the safe
shell-escaped sequence) or otherwise ensure the URL is safely quoted, then use
the sanitized value when creating gitClone; alternatively, tighten the CRD
validation in softwarebuild_types.go to forbid single quotes. Ensure the change
targets the gitClone construction site so the revision and other fields remain
unchanged.
- Around line 139-150: The git clone construction in
GenerateSoftwareBuildPipelineRun is duplicated and its local fetchCommand is
unused; remove this dead block and consolidate the logic by extracting a single
helper (e.g., computeFetchCommand or getFetchCommand) that uses safeGitRefRe and
sb.Spec.Source.Git.Revision/URL, then call that helper from both
GenerateSoftwareBuildPipelineRun and buildPipelineRunParams (or have
buildPipelineRunParams be the single caller) so the PipelineRun params receive
the computed fetchCommand consistently.
---
Nitpick comments:
In `@api/v1alpha1/softwarebuild_types.go`:
- Around line 111-118: The SoftwareBuildPipelineStages struct currently has all
five fields required; add Kubernetes optional markers by placing a `//
+optional` comment above each field (Fetch, Prebuild, Build, Postbuild, Deploy)
in the SoftwareBuildPipelineStages definition so these stages can be omitted by
users; ensure the json tags remain unchanged (e.g., `json:"fetch"`) and keep the
type as SoftwareBuildStageSpec unless you also intend to convert fields to
pointer types—only the `// +optional` comments are required here.
In `@internal/common/tasks/software_build_test.go`:
- Around line 344-367: Add a new unit test that mirrors
TestGenerateSoftwareBuildPipelineRun_UnsafeRevisionSanitized but targets
malicious URLs with embedded single quotes (e.g.,
"https://evil.com/'$(whoami)'"); in the test (e.g.,
TestGenerateSoftwareBuildPipelineRun_URLWithQuotesSanitized) construct a
SoftwareBuild with that Git URL and call GenerateSoftwareBuildPipelineRun, find
the "fetchCommand" param in pr.Spec.Params, and assert that the produced
p.Value.StringVal neither contains the embedded command substitution ($(whoami))
nor unescaped single-quote characters—this verifies URL inputs are rejected or
properly escaped by the code that builds the fetch command in
GenerateSoftwareBuildPipelineRun.
In `@internal/controller/softwarebuild/controller.go`:
- Around line 189-202: loadBuildConfig currently swallows all Get errors; change
it to distinguish NotFound from other errors and log/report non-NotFound
failures. In loadBuildConfig (SoftwareBuildReconciler) import
k8s.io/apimachinery/pkg/api/errors as apierrors, then replace the single if err
!= nil { return nil } with: if apierrors.IsNotFound(err) { return nil } else {
r.Log.Error(err, "failed to get OperatorConfig", "namespace", namespace) ;
return nil } so that RBAC/API errors are surfaced in logs (optionally consider
returning an error from loadBuildConfig later for caller handling); keep the
rest of the function returning &tasks.BuildConfig when opConfig present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1d47cf58-0260-46e9-87c0-a2f0c952a712
📒 Files selected for processing (9)
api/v1alpha1/softwarebuild_types.goapi/v1alpha1/softwarebuild_types_test.goapi/v1alpha1/zz_generated.deepcopy.gointernal/common/tasks/software_build.gointernal/common/tasks/software_build_test.gointernal/common/tasks/tasks.gointernal/controller/operatorconfig/controller.gointernal/controller/softwarebuild/controller.gointernal/controller/softwarebuild/controller_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
- internal/controller/softwarebuild/controller_test.go
- api/v1alpha1/softwarebuild_types_test.go
- internal/controller/operatorconfig/controller.go
- api/v1alpha1/zz_generated.deepcopy.go
… formatting The integration test still referenced SoftwareBuildSourceHostPath and SoftwareBuildHostPathSource which were removed in the prior commit (finding centos-automotive-suite#7). Replace with PVC source type. Also fix gofmt alignment in SoftwareBuildSpec struct fields. Made-with: Cursor
…njection - Add +kubebuilder:validation:Minimum=0 to TimeoutSeconds to reject negative values at the API level. - Remove duplicate git clone construction in GenerateSoftwareBuildPipelineRun (dead code — only the copy in buildPipelineRunParams is used). - Tighten git URL CRD validation pattern to also exclude single quotes, double quotes, and backticks, closing a shell injection vector. Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
api/v1alpha1/softwarebuild_types.go (1)
135-136:⚠️ Potential issue | 🟡 MinorAdd non-negative validation for
timeoutSeconds.Negative values are currently accepted at the API level and then ignored by timeout resolution logic, which is ambiguous for users. Add CRD validation to reject negatives.
Suggested patch
type SoftwareBuildSpec struct { // +optional Runtime SoftwareBuildRuntimeSpec `json:"runtime,omitempty"` Source SoftwareBuildSourceSpec `json:"source"` Stages SoftwareBuildPipelineStages `json:"stages"` Destination SoftwareBuildDestinationSpec `json:"destination"` + // +kubebuilder:validation:Minimum=0 // +optional TimeoutSeconds int64 `json:"timeoutSeconds,omitempty"` }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/v1alpha1/softwarebuild_types.go` around lines 135 - 136, Add a non-negative CRD validation tag to the TimeoutSeconds field so negative values are rejected at the API level: add the kubebuilder validation marker (Minimum=0) above the TimeoutSeconds field (the field name is TimeoutSeconds on the SoftwareBuild spec struct) and then re-generate the CRD manifests (controller-gen) so the validation appears in the CRD. Ensure the existing json tag remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api/v1alpha1/softwarebuild_types.go`:
- Around line 85-86: SoftwareBuildPVCSource.Path is exposed in the API but
ignored by the pipeline workspace creation; update the workspace-generation code
that builds the PVC workspace (the function in software_build.go that converts a
SoftwareBuildPVCSource into a Pipeline/Task workspace) to use
SoftwareBuildPVCSource.Path as the mount subPath (or equivalent field on the
Pipeline Workspace/WorkspaceDeclaration) so a non-root path mounts the PVC at
the specified sub-directory (treat the default "/" as “no subPath”);
alternatively, if you choose not to implement it now, mark
SoftwareBuildPVCSource.Path as not implemented by adding the same sentinel
comment/annotation used for credentialsSecretRef and update the API docs and
unit tests to reflect the no-op state.
---
Duplicate comments:
In `@api/v1alpha1/softwarebuild_types.go`:
- Around line 135-136: Add a non-negative CRD validation tag to the
TimeoutSeconds field so negative values are rejected at the API level: add the
kubebuilder validation marker (Minimum=0) above the TimeoutSeconds field (the
field name is TimeoutSeconds on the SoftwareBuild spec struct) and then
re-generate the CRD manifests (controller-gen) so the validation appears in the
CRD. Ensure the existing json tag remains unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 97b8d7ea-eb7a-4d59-a854-79d1bf5a27eb
📒 Files selected for processing (2)
api/v1alpha1/softwarebuild_types.gointernal/controller/test/softwarebuild_controller_test.go
✅ Files skipped from review due to trivial changes (1)
- internal/controller/test/softwarebuild_controller_test.go
SoftwareBuildPVCSource.Path was exposed in the API but ignored by buildWorkspaceBinding. Now sets SubPath on the workspace binding when path is non-empty and not "/". Adds tests for SubPath wiring and root path no-op. Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/common/tasks/software_build.go`:
- Around line 141-155: The PipelineRun is being created in sb.Namespace (use of
sb.Namespace in the PipelineRun ObjectMeta) which prevents the PipelineRef
(SoftwareBuildPipelineName) from being resolved when the shared pipeline is
installed in the OperatorConfig namespace; update the generator to accept and
use the operator namespace (from OperatorConfig/default) instead of sb.Namespace
when constructing the PipelineRun ObjectMeta.Namespace so PipelineRef resolves
correctly (ensure buildPipelineRunParams and buildWorkspaceBinding still receive
any needed values but do not determine the PipelineRun namespace), or
alternatively constrain SoftwareBuild reconciliation to only run when
sb.Namespace equals the operator namespace and add a clear guard in the code
paths that call the PipelineRun construction.
- Around line 199-205: The fallback for stageImage is incorrectly set to the
literal string "$(params.containerImage)" which will be passed through
unchanged; instead use the in-scope variable globalImage (the same value set for
the "containerImage" param) when s.image == "" so the PipelineRun param value
contains the actual image string; update the logic around stages -> stageImage
(used when appending tektonv1.Param{Name: fmt.Sprintf("%sImage", s.name), ...})
to default to globalImage rather than the literal "$(params.containerImage)".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: bdf7f4c1-f5e8-4e96-9755-6bf07ffbe723
📒 Files selected for processing (2)
api/v1alpha1/softwarebuild_types.gointernal/common/tasks/software_build.go
…sues - Place PipelineRun in the operator namespace so PipelineRef resolves the shared Pipeline deployed by OperatorConfig (CodeRabbit #3069477143) - Use resolved globalImage instead of literal $(params.containerImage) which is unsupported in PipelineRun spec.params[].value (CodeRabbit #3069477145) - Fix all 14 golangci-lint issues: goconst, revive (exported comments, stutter rename SoftwareBuildReconciler→Reconciler), staticcheck (tagged switch, embedded Duration selector), unconvert Made-with: Cursor
|
Addressed linter erros and all review comments. |
The backtick in the kubebuilder validation marker broke controller-gen parsing since raw string literals delimited by backticks cannot contain a literal backtick. Dropped backtick from the exclusion character class; single/double quotes still block the primary injection vectors. Regenerated CRD manifests. Made-with: Cursor
…tests The envtest suite was missing tektonv1.AddToScheme so PipelineRun creation failed with notRegisteredErr. Also set the Log and OperatorNamespace fields added by the Reconciler refactoring. Made-with: Cursor
The envtest kube-apiserver needs CRD definitions for any custom resources the tests interact with. Added minimal Tekton Pipeline and PipelineRun CRDs in testdata/ so the reconciler can create PipelineRun objects during the integration test. Made-with: Cursor
| pr := tasks.GenerateSoftwareBuildPipelineRun(sb, config, r.OperatorNamespace) | ||
|
|
||
| if err := controllerutil.SetControllerReference(sb, pr, r.Scheme); err != nil { | ||
| return ctrl.Result{}, fmt.Errorf("setting controller reference: %w", err) | ||
| } |
There was a problem hiding this comment.
CRITICAL - I think GenerateSoftwareBuildPipelineRun places the PipelineRun in r.OperatorNamespace (hardcoded to "automotive-dev-operator-system"), but controllerutil.SetControllerReference at line 63 will always fail with "cross-namespace owner references are disallowed" when the SoftwareBuild CR lives in any other namespace. The error is returned before r.Create is reached, so the PipelineRun is never created and the reconciler retries indefinitely.
Also, the Owns(&tektonv1.PipelineRun{}) watch relies on owner references that are never set, so PipelineRun status changes won't trigger reconciliation.
The existing ImageBuild controller avoids this by creating PipelineRuns in imageBuild.Namespace.
| Script: `#!/usr/bin/env bash | ||
| set -euo pipefail | ||
| cd $(workspaces.ws.path) | ||
| bash -lc "$(params.command)" | ||
| `, |
There was a problem hiding this comment.
[HIGH] Step script double-quotes the user command, breaking quoted commands and variable references.
The step script embeds the user-provided command inside double quotes: bash -lc "$(params.command)". Tekton performs literal text substitution on $(params.command) before the shell sees the script. If the user's command contains double quotes (e.g., grep "pattern" file), the quoting structure breaks. If the command contains $VAR references, the outer bash (with set -u) expands them before bash -lc executes, causing unbound variable errors.
Suggestion: write the command to a file via a single-quoted heredoc to prevent double-expansion:
Script: `#!/usr/bin/env bash
set -euo pipefail
cd $(workspaces.ws.path)
cat > /tmp/stage-cmd.sh <<'TEKTON_STAGE_EOF'
$(params.command)
TEKTON_STAGE_EOF
bash -l /tmp/stage-cmd.sh
`,AI-generated, human reviewed
|
|
||
| func (r *Reconciler) loadBuildConfig(ctx context.Context) *tasks.BuildConfig { | ||
| var opConfig automotivev1alpha1.OperatorConfig | ||
| if err := r.Get(ctx, types.NamespacedName{Name: "default", Namespace: r.OperatorNamespace}, &opConfig); err != nil { |
There was a problem hiding this comment.
[HIGH] OperatorConfig lookup uses wrong name "default" instead of "config".
loadBuildConfig fetches NamespacedName{Name: "default", ...} but every other controller in the codebase uses Name: "config" (confirmed across imagebuild/controller.go, containerbuild/controller.go, and the sample manifest config/samples/automotive_v1_operatorconfig.yaml). The lookup always returns NotFound, so loadBuildConfig always returns nil and the OperatorConfig settings (DefaultImage, PVCSize, BuildTimeoutMinutes) are never applied.
| func (r *Reconciler) loadBuildConfig(ctx context.Context) *tasks.BuildConfig { | ||
| var opConfig automotivev1alpha1.OperatorConfig | ||
| if err := r.Get(ctx, types.NamespacedName{Name: "default", Namespace: r.OperatorNamespace}, &opConfig); err != nil { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
[HIGH] loadBuildConfig silently swallows transient API errors, permanently baking wrong defaults into PipelineRuns.
The function returns nil on any error from r.Get, including transient errors (network timeouts, RBAC failures, leader election failovers), not just NotFound. When nil is returned, GenerateSoftwareBuildPipelineRun falls back to hardcoded defaults (1Gi PVC, no timeout, ubuntu:24.04 image). Because the PipelineRun name is deterministic, this misconfigured PipelineRun is created once and never recreated.
Generally speaking, most errors should be handled properly and not hidden as it cpould impact reproducibility.
| } else { | ||
| if err := r.cleanupSoftwareBuilds(ctx, config); err != nil { | ||
| log.Error(err, "Failed to cleanup SoftwareBuilds pipeline") |
There was a problem hiding this comment.
[HIGH] cleanupSoftwareBuilds error is logged but not propagated, leaving orphaned Pipeline resources.
When SoftwareBuilds is disabled, the cleanup error is logged but not returned, so reconciliation reports success while stale Pipelines persist. Compare with the OSBuilds cleanup earlier in this file, which sets Phase=Failed, updates the status message, and returns the error so reconciliation retries.
Suggestion: handle the error consistently with the OSBuilds cleanup pattern: set Phase=Failed, update the status message, and return the error.
| // SoftwareBuildPipelineStages groups the five sequential stages. | ||
| type SoftwareBuildPipelineStages struct { | ||
| Fetch SoftwareBuildStageSpec `json:"fetch"` | ||
| Prebuild SoftwareBuildStageSpec `json:"prebuild"` | ||
| Build SoftwareBuildStageSpec `json:"build"` | ||
| Postbuild SoftwareBuildStageSpec `json:"postbuild"` | ||
| Deploy SoftwareBuildStageSpec `json:"deploy"` | ||
| } |
There was a problem hiding this comment.
i think thats okay but the root cause #199 (comment) remains unanswered.
| /* | ||
| Copyright 2025. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| package v1alpha1 | ||
|
|
||
| import ( | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| ) | ||
|
|
||
| // SoftwareBuildSourceType identifies where source code is obtained from. | ||
| type SoftwareBuildSourceType string | ||
|
|
||
| const ( | ||
| SoftwareBuildSourceGit SoftwareBuildSourceType = "git" | ||
| SoftwareBuildSourcePVC SoftwareBuildSourceType = "pvc" | ||
| SoftwareBuildSourceHostPath SoftwareBuildSourceType = "hostPath" | ||
| ) | ||
|
|
||
| // SoftwareBuildDestinationType identifies where build artifacts are stored. | ||
| type SoftwareBuildDestinationType string | ||
|
|
||
| const ( | ||
| SoftwareBuildDestSharedFolder SoftwareBuildDestinationType = "sharedFolder" | ||
| SoftwareBuildDestRegistry SoftwareBuildDestinationType = "registry" | ||
| SoftwareBuildDestArtifactory SoftwareBuildDestinationType = "artifactory" | ||
| SoftwareBuildDestQuay SoftwareBuildDestinationType = "quay" | ||
| ) | ||
|
|
||
| // SoftwareBuildPhase represents the current lifecycle phase. | ||
| type SoftwareBuildPhase string | ||
|
|
||
| const ( | ||
| SoftwareBuildPhasePending SoftwareBuildPhase = "Pending" | ||
| SoftwareBuildPhaseRunning SoftwareBuildPhase = "Running" | ||
| SoftwareBuildPhaseSucceeded SoftwareBuildPhase = "Succeeded" | ||
| SoftwareBuildPhaseFailed SoftwareBuildPhase = "Failed" | ||
| ) | ||
|
|
||
| // SoftwareBuildSecretReference points to a Kubernetes Secret. | ||
| type SoftwareBuildSecretReference struct { | ||
| // +kubebuilder:validation:MinLength=1 | ||
| Name string `json:"name"` | ||
| // +optional | ||
| Key string `json:"key,omitempty"` | ||
| } | ||
|
|
||
| // SoftwareBuildRuntimeSpec configures the container environment used for every | ||
| // pipeline stage unless overridden per-stage. | ||
| type SoftwareBuildRuntimeSpec struct { | ||
| // Image is the container image that provides the build toolchain. | ||
| // +kubebuilder:default="ubuntu:24.04" | ||
| // +kubebuilder:validation:MinLength=1 | ||
| Image string `json:"image,omitempty"` | ||
|
|
||
| // ServiceAccountName is the Kubernetes SA the build pod runs as. | ||
| // +optional | ||
| ServiceAccountName string `json:"serviceAccountName,omitempty"` | ||
| } | ||
|
|
||
| // SoftwareBuildGitSource describes a Git repository to clone. | ||
| type SoftwareBuildGitSource struct { | ||
| // +kubebuilder:validation:Pattern=`^https?://|^git@` | ||
| URL string `json:"url"` | ||
| // +kubebuilder:default=main | ||
| Revision string `json:"revision,omitempty"` | ||
| // +optional | ||
| CredentialsSecretRef *SoftwareBuildSecretReference `json:"credentialsSecretRef,omitempty"` | ||
| } | ||
|
|
||
| // SoftwareBuildPVCSource references an existing PVC. | ||
| type SoftwareBuildPVCSource struct { | ||
| // +kubebuilder:validation:MinLength=1 | ||
| ClaimName string `json:"claimName"` | ||
| // +kubebuilder:default=/ | ||
| Path string `json:"path,omitempty"` | ||
| } | ||
|
|
||
| // SoftwareBuildHostPathSource mounts a node-local directory. | ||
| type SoftwareBuildHostPathSource struct { | ||
| // +kubebuilder:validation:MinLength=1 | ||
| Path string `json:"path"` | ||
| } | ||
|
|
||
| // SoftwareBuildSourceSpec identifies the code location. | ||
| type SoftwareBuildSourceSpec struct { | ||
| // +kubebuilder:validation:Enum=git;pvc;hostPath | ||
| Type SoftwareBuildSourceType `json:"type"` | ||
| // +optional | ||
| Git *SoftwareBuildGitSource `json:"git,omitempty"` | ||
| // +optional | ||
| PVC *SoftwareBuildPVCSource `json:"pvc,omitempty"` | ||
| // +optional | ||
| HostPath *SoftwareBuildHostPathSource `json:"hostPath,omitempty"` | ||
| } | ||
|
|
||
| // SoftwareBuildStageSpec defines a single pipeline stage. | ||
| type SoftwareBuildStageSpec struct { | ||
| // Command is executed via bash inside the runtime image. | ||
| // +kubebuilder:validation:MinLength=1 | ||
| Command string `json:"command"` | ||
| // Image overrides the runtime image for this stage only. | ||
| // +optional | ||
| Image string `json:"image,omitempty"` | ||
| } | ||
|
|
||
| // SoftwareBuildPipelineStages groups the five sequential stages. | ||
| type SoftwareBuildPipelineStages struct { | ||
| Fetch SoftwareBuildStageSpec `json:"fetch"` | ||
| Prebuild SoftwareBuildStageSpec `json:"prebuild"` | ||
| Build SoftwareBuildStageSpec `json:"build"` | ||
| Postbuild SoftwareBuildStageSpec `json:"postbuild"` | ||
| Deploy SoftwareBuildStageSpec `json:"deploy"` | ||
| } | ||
|
|
||
| // SoftwareBuildDestinationSpec describes where artifacts go. | ||
| type SoftwareBuildDestinationSpec struct { | ||
| // +kubebuilder:validation:Enum=sharedFolder;registry;artifactory;quay | ||
| Type SoftwareBuildDestinationType `json:"type"` | ||
| // +optional | ||
| Path string `json:"path,omitempty"` | ||
| // +optional | ||
| Repository string `json:"repository,omitempty"` | ||
| // +optional | ||
| CredentialsSecretRef *SoftwareBuildSecretReference `json:"credentialsSecretRef,omitempty"` | ||
| } | ||
|
|
||
| // SoftwareBuildSpec defines the desired state of SoftwareBuild. | ||
| type SoftwareBuildSpec struct { | ||
| // +optional | ||
| Runtime SoftwareBuildRuntimeSpec `json:"runtime,omitempty"` | ||
| Source SoftwareBuildSourceSpec `json:"source"` | ||
| Stages SoftwareBuildPipelineStages `json:"stages"` | ||
| Destination SoftwareBuildDestinationSpec `json:"destination"` | ||
| // +optional | ||
| TimeoutSeconds int64 `json:"timeoutSeconds,omitempty"` | ||
| } | ||
|
|
||
| // SoftwareBuildStageStatus captures per-stage progress. | ||
| type SoftwareBuildStageStatus struct { | ||
| Name string `json:"name,omitempty"` | ||
| // +optional | ||
| StartedAt *metav1.Time `json:"startedAt,omitempty"` | ||
| // +optional | ||
| FinishedAt *metav1.Time `json:"finishedAt,omitempty"` | ||
| // +optional | ||
| State string `json:"state,omitempty"` | ||
| // +optional | ||
| Message string `json:"message,omitempty"` | ||
| } | ||
|
|
||
| // SoftwareBuildStatus defines the observed state of SoftwareBuild. | ||
| type SoftwareBuildStatus struct { | ||
| // +optional | ||
| Phase SoftwareBuildPhase `json:"phase,omitempty"` | ||
| // +optional | ||
| PipelineRunName string `json:"pipelineRunName,omitempty"` | ||
| // +optional | ||
| ArtifactURI string `json:"artifactURI,omitempty"` | ||
| // +optional | ||
| FailureReason string `json:"failureReason,omitempty"` | ||
| // +optional | ||
| Stages []SoftwareBuildStageStatus `json:"stages,omitempty"` | ||
| // +optional | ||
| Conditions []metav1.Condition `json:"conditions,omitempty"` | ||
| } | ||
|
|
||
| // +kubebuilder:object:root=true | ||
| // +kubebuilder:subresource:status | ||
| // +kubebuilder:resource:path=softwarebuilds,scope=Namespaced,shortName=sb | ||
| // +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase` | ||
| // +kubebuilder:printcolumn:name="Image",type=string,JSONPath=`.spec.runtime.image` | ||
| // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` | ||
|
|
||
| // SoftwareBuild is the Schema for the softwarebuilds API. | ||
| // It drives a generic, stage-based Tekton pipeline that can build software | ||
| // for any target OS or toolchain by specifying a runtime container image and | ||
| // five sequential shell commands. | ||
| type SoftwareBuild struct { | ||
| metav1.TypeMeta `json:",inline"` | ||
| metav1.ObjectMeta `json:"metadata,omitempty"` | ||
|
|
||
| Spec SoftwareBuildSpec `json:"spec,omitempty"` | ||
| Status SoftwareBuildStatus `json:"status,omitempty"` | ||
| } | ||
|
|
||
| // +kubebuilder:object:root=true | ||
|
|
||
| // SoftwareBuildList contains a list of SoftwareBuild. | ||
| type SoftwareBuildList struct { | ||
| metav1.TypeMeta `json:",inline"` | ||
| metav1.ListMeta `json:"metadata,omitempty"` | ||
| Items []SoftwareBuild `json:"items"` | ||
| } | ||
|
|
||
| func init() { | ||
| SchemeBuilder.Register(&SoftwareBuild{}, &SoftwareBuildList{}) | ||
| } |
There was a problem hiding this comment.
This AI generated response does not address the actual point. ImageBuild validates distro/target/mode, has mode-specific logic, orchestrates exports, manages hardware flashing, tracks provenance. SoftwareBuild passes a string to bash. Calling this "the same pattern" is wrong. One transforms, the other proxies.
The target audience argument would hold if the CRD gave them something but it does not. Users still write raw shell commands and debug their own failures. The CRD hides YAML syntax, which a Pipeline template, a shell script wrapping tkn or Helm chart also does without introducing a new API surface to maintain.
"Operator-managed lifecycle" is what every controller does by definition. That is not domain-specific value, that is the baseline.
Five stages map 1:1 to five tasks, source types map 1:1 to tekton workspace bindings.
TL;DR: None of these facts from the original review were disputed or resolved hence I am asking for further changes and discussion.
| Steps: []tektonv1.Step{ | ||
| { | ||
| Name: "run", | ||
| Image: "$(params.image)", | ||
| ImagePullPolicy: corev1.PullIfNotPresent, | ||
| Script: `#!/usr/bin/env bash | ||
| set -euo pipefail | ||
| cd $(workspaces.ws.path) | ||
| bash -lc "$(params.command)" | ||
| `, | ||
| }, | ||
| }, |
| func softwareBuildStageTask(stageName, paramImage, paramCommand string) tektonv1.PipelineTask { | ||
| return tektonv1.PipelineTask{ | ||
| Name: stageName, | ||
| TaskSpec: &tektonv1.EmbeddedTask{ | ||
| TaskSpec: tektonv1.TaskSpec{ | ||
| Params: []tektonv1.ParamSpec{ | ||
| {Name: "image", Type: tektonv1.ParamTypeString}, | ||
| {Name: "command", Type: tektonv1.ParamTypeString}, | ||
| }, | ||
| Workspaces: []tektonv1.WorkspaceDeclaration{ | ||
| {Name: "ws", MountPath: "/workspace"}, | ||
| }, | ||
| Steps: []tektonv1.Step{ | ||
| { | ||
| Name: "run", | ||
| Image: "$(params.image)", | ||
| ImagePullPolicy: corev1.PullIfNotPresent, | ||
| Script: `#!/usr/bin/env bash | ||
| set -euo pipefail | ||
| cd $(workspaces.ws.path) | ||
| bash -lc "$(params.command)" | ||
| `, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| Params: []tektonv1.Param{ | ||
| {Name: "image", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: paramImage}}, | ||
| {Name: "command", Value: tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: paramCommand}}, | ||
| }, | ||
| Workspaces: []tektonv1.WorkspacePipelineTaskBinding{ | ||
| {Name: "ws", Workspace: "shared-workspace"}, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // GenerateSoftwareBuildPipeline creates the Tekton Pipeline that runs five | ||
| // sequential stages inside a user-chosen container image. | ||
| func GenerateSoftwareBuildPipeline(name, namespace string, _ *BuildConfig) *tektonv1.Pipeline { | ||
| stages := []string{"fetch", "prebuild", "build", "postbuild", "deploy"} | ||
|
|
||
| tasks := make([]tektonv1.PipelineTask, len(stages)) | ||
| for i, s := range stages { | ||
| tasks[i] = softwareBuildStageTask( | ||
| s, | ||
| "$(params.containerImage)", | ||
| fmt.Sprintf("$(params.%sCommand)", s), | ||
| ) | ||
| if i > 0 { | ||
| tasks[i].RunAfter = []string{stages[i-1]} | ||
| } | ||
| } | ||
|
|
||
| return &tektonv1.Pipeline{ | ||
| TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "Pipeline"}, | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: name, | ||
| Namespace: namespace, | ||
| Labels: map[string]string{ | ||
| "app.kubernetes.io/managed-by": "automotive-dev-operator", | ||
| }, | ||
| }, | ||
| Spec: tektonv1.PipelineSpec{ | ||
| Params: []tektonv1.ParamSpec{ | ||
| { | ||
| Name: "containerImage", Type: tektonv1.ParamTypeString, | ||
| Default: &tektonv1.ParamValue{Type: tektonv1.ParamTypeString, StringVal: defaultSoftwareBuildImage}, | ||
| Description: "Container image providing the build toolchain", | ||
| }, | ||
| {Name: "fetchCommand", Type: tektonv1.ParamTypeString, Description: "Fetch stage command"}, | ||
| {Name: "prebuildCommand", Type: tektonv1.ParamTypeString, Description: "Prebuild stage command"}, | ||
| {Name: "buildCommand", Type: tektonv1.ParamTypeString, Description: "Build stage command"}, | ||
| {Name: "postbuildCommand", Type: tektonv1.ParamTypeString, Description: "Postbuild stage command"}, | ||
| {Name: "deployCommand", Type: tektonv1.ParamTypeString, Description: "Deploy stage command"}, | ||
| }, | ||
| Workspaces: []tektonv1.PipelineWorkspaceDeclaration{ | ||
| {Name: "shared-workspace"}, | ||
| }, | ||
| Tasks: tasks, | ||
| }, | ||
| } | ||
| } |
| func buildWorkspaceBinding(sb *automotivev1alpha1.SoftwareBuild, pvcSize string) []tektonv1.WorkspaceBinding { | ||
| if sb.Spec.Source.Type == automotivev1alpha1.SoftwareBuildSourcePVC && sb.Spec.Source.PVC != nil { | ||
| return []tektonv1.WorkspaceBinding{ | ||
| { | ||
| Name: "shared-workspace", | ||
| PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ | ||
| ClaimName: sb.Spec.Source.PVC.ClaimName, | ||
| }, | ||
| }, | ||
| } | ||
| } |
There was a problem hiding this comment.
i think you addressed this in one of your latest commits. seems fine for now.
- Add CEL validation for PVC path (no '..' traversal) - Remove unimplemented CredentialsSecretRef and SoftwareBuildSecretReference - Rename SoftwareBuildDestSharedFolder → SoftwareBuildDestinationSharedFolder - Rename loop var 'cr' → 'childRef' in buildStageStatuses - Rename safeGitRefRe → safeGitRefPattern - Replace restating block comments with meaningful descriptions - Log warning when parsePVCSize receives invalid OperatorConfig value - Extract shared BuildConfigFromSoftwareBuilds helper (DRY) - Use generic status message to avoid leaking internal error details - Integration test now verifies PipelineRun creation and status fields Made-with: Cursor
There was a problem hiding this comment.
♻️ Duplicate comments (2)
internal/controller/softwarebuild/controller.go (2)
197-203:⚠️ Potential issue | 🟠 Major
loadBuildConfighas two unresolved issues from past review.
Line 199: Uses
Name: "default"but other controllers in the codebase useName: "config"for the OperatorConfig. This mismatch means the lookup will always return NotFound, and config settings won't be applied.Lines 200-201: Returns
nilon any error, including transient API errors (network timeouts, RBAC failures). This silently bakes hardcoded defaults into the PipelineRun. Since the PipelineRun name is deterministic, the misconfigured run is created once and never recreated.🛠️ Proposed fix
func (r *Reconciler) loadBuildConfig(ctx context.Context) *tasks.BuildConfig { var opConfig automotivev1alpha1.OperatorConfig - if err := r.Get(ctx, types.NamespacedName{Name: "default", Namespace: r.OperatorNamespace}, &opConfig); err != nil { - return nil + if err := r.Get(ctx, types.NamespacedName{Name: "config", Namespace: r.OperatorNamespace}, &opConfig); err != nil { + if !errors.IsNotFound(err) { + r.Log.Error(err, "failed to load OperatorConfig, using defaults") + } + return nil } return tasks.BuildConfigFromSoftwareBuilds(opConfig.Spec.SoftwareBuilds) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/controller/softwarebuild/controller.go` around lines 197 - 203, The loadBuildConfig function currently looks up OperatorConfig with Name "default" and swallows all errors; change Name "default" to "config" to match other controllers, and update loadBuildConfig signature to return (*tasks.BuildConfig, error) so callers can handle API errors; inside the function call r.Get(..., &opConfig) and if err != nil return (nil, nil) only when apierrors.IsNotFound(err) otherwise return (nil, err); on success return (tasks.BuildConfigFromSoftwareBuilds(opConfig.Spec.SoftwareBuilds), nil) and update callers to handle the error (requeue/retry) instead of silently using defaults.
59-76:⚠️ Potential issue | 🟠 MajorCross-namespace owner reference will fail if SoftwareBuild is created in a namespace different from the operator namespace.
Line 61 places the PipelineRun in
r.OperatorNamespace("automotive-dev-operator-system"), but line 63 setssbas the controller owner. If the SoftwareBuild was created in a different namespace,SetControllerReferencewill fail with a "cross-namespace owner references are disallowed" error (line 63–65 returns immediately on error, blocking PipelineRun creation).The controller watches all namespaces (line 208:
For(&automotivev1alpha1.SoftwareBuild{})has no namespace predicate), and the CRD does not validate or restrict where SoftwareBuild resources can be created, making this scenario possible. Additionally, theOwns(&tektonv1.PipelineRun{})watch on line 209 relies on owner references that won't be set for cross-namespace cases.Options:
- Document/enforce that SoftwareBuild must be created only in the operator namespace (either via CRD validation webhook or controller-level filtering)
- Skip owner reference for cross-namespace and use label-based association instead (similar to the existing
automotive.sdv.cloud.redhat.com/softwarebuildlabel logic)- Create PipelineRun in
sb.Namespace(but PipelineRef and other operator resources may not resolve)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/controller/softwarebuild/controller.go` around lines 59 - 76, The createPipelineRun function currently always places the PipelineRun in r.OperatorNamespace and unconditionally calls controllerutil.SetControllerReference(sb, pr, r.Scheme), which fails for cross-namespace SoftwareBuilds; modify createPipelineRun to detect when sb.Namespace != r.OperatorNamespace and in that case do NOT call SetControllerReference but instead associate the PipelineRun to the SoftwareBuild via labels (e.g., reuse the existing automotive.sdv.cloud.redhat.com/softwarebuild label and set sb.Status.PipelineRunName/update status accordingly), while preserving the current controllerutil.SetControllerReference behavior for same-namespace cases; update any comments/logging to note the cross-namespace fallback so the Owns() watch can be supplemented by label-based lookup for cross-namespace resources.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@internal/controller/softwarebuild/controller.go`:
- Around line 197-203: The loadBuildConfig function currently looks up
OperatorConfig with Name "default" and swallows all errors; change Name
"default" to "config" to match other controllers, and update loadBuildConfig
signature to return (*tasks.BuildConfig, error) so callers can handle API
errors; inside the function call r.Get(..., &opConfig) and if err != nil return
(nil, nil) only when apierrors.IsNotFound(err) otherwise return (nil, err); on
success return
(tasks.BuildConfigFromSoftwareBuilds(opConfig.Spec.SoftwareBuilds), nil) and
update callers to handle the error (requeue/retry) instead of silently using
defaults.
- Around line 59-76: The createPipelineRun function currently always places the
PipelineRun in r.OperatorNamespace and unconditionally calls
controllerutil.SetControllerReference(sb, pr, r.Scheme), which fails for
cross-namespace SoftwareBuilds; modify createPipelineRun to detect when
sb.Namespace != r.OperatorNamespace and in that case do NOT call
SetControllerReference but instead associate the PipelineRun to the
SoftwareBuild via labels (e.g., reuse the existing
automotive.sdv.cloud.redhat.com/softwarebuild label and set
sb.Status.PipelineRunName/update status accordingly), while preserving the
current controllerutil.SetControllerReference behavior for same-namespace cases;
update any comments/logging to note the cross-namespace fallback so the Owns()
watch can be supplemented by label-based lookup for cross-namespace resources.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 71a15cc0-520b-4230-90c2-49dbdd475355
⛔ Files ignored due to path filters (1)
config/crd/bases/automotive.sdv.cloud.redhat.com_softwarebuilds.yamlis excluded by!config/crd/bases/**
📒 Files selected for processing (8)
api/v1alpha1/softwarebuild_types.goapi/v1alpha1/softwarebuild_types_test.goapi/v1alpha1/zz_generated.deepcopy.gointernal/common/tasks/software_build.gointernal/common/tasks/tasks.gointernal/controller/operatorconfig/controller.gointernal/controller/softwarebuild/controller.gointernal/controller/test/softwarebuild_controller_test.go
✅ Files skipped from review due to trivial changes (2)
- internal/controller/test/softwarebuild_controller_test.go
- api/v1alpha1/zz_generated.deepcopy.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/controller/operatorconfig/controller.go
- api/v1alpha1/softwarebuild_types_test.go
|
This PR is an initial implementation — a foundation, not a finished product. The review surfaced valid points and we've addressed the concrete ones: path traversal validation, dead code removal, naming consistency, test coverage, error handling, and DRY extraction. The remaining open threads (Chains integration, stage status granularity, SA allowlisting) are tracked in follow-up issues (#200, #201, #204). On the broader design question — SoftwareBuild uses a CRD as an API layer intentionally. Today it's thin. That's the point. The CRD gives us a control plane surface to build on: Parity with ImageBuild: both build paths share the same operational model — CR in, reconciler manages lifecycle, Tekton executes. One API surface, one set of RBAC rules, one CLI (caib), one monitoring stack. Without the CRD, SoftwareBuild becomes a second-class citizen that users interact with differently. Future leverage: shared build caches, compliance gating, build tiers, AI-assisted configuration, and developer onboarding flows all need a stable API object to attach to. A Pipeline template or Helm chart gives us none of that. Extensibility without breakage: when Chains integration, artifact tracking, or new source types land, the CRD absorbs them as new fields and controller logic — no user-facing migration. The five fixed stages, the inline TaskSpec, the minimal status — all of these are starting points that will evolve. Changes and suggestions are welcome. The architecture is what matters here: a CRD-based control plane that keeps software builds on the same footing as image builds. |
Summary by CodeRabbit
New Features
Documentation
Tests