Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,13 @@ resources:
kind: OperatorConfig
path: github.com/centos-automotive-suite/automotive-dev-operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: sdv.cloud.redhat.com
group: automotive
kind: ScheduledImageBuild
path: github.com/centos-automotive-suite/automotive-dev-operator/api/v1alpha1
version: v1alpha1
version: "3"
5 changes: 5 additions & 0 deletions api/v1alpha1/catalogimage_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ type CatalogImageStatus struct {
// +optional
PublishedAt *metav1.Time `json:"publishedAt,omitempty"`

// VerificationFailures counts consecutive verification failures.
// After the max is reached the phase transitions to Failed.
// +optional
VerificationFailures int32 `json:"verificationFailures,omitempty"`

// SourceImageBuild references the ImageBuild that created this catalog entry
// +optional
SourceImageBuild string `json:"sourceImageBuild,omitempty"`
Expand Down
13 changes: 11 additions & 2 deletions api/v1alpha1/imagebuild_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func IsTerminalBuildPhase(phase string) bool {
// ImageBuildSpec defines the desired state of ImageBuild
// +kubebuilder:printcolumn:name="StorageClass",type=string,JSONPath=`.spec.storageClass`
// +kubebuilder:validation:XValidation:rule="!has(self.reproducible) || !self.reproducible || self.secureBuild",message="reproducible builds require secureBuild to be true"
// +kubebuilder:validation:XValidation:rule="!(has(self.export) && has(self.export.disk) && has(self.export.disk.oci) && size(self.export.disk.oci) > 0) || size(self.secretRef) > 0 || (has(self.export) && has(self.export.useServiceAccountAuth) && self.export.useServiceAccountAuth)",message="secretRef is required when export.disk.oci is set (unless useServiceAccountAuth is true)"
// +kubebuilder:validation:XValidation:rule="!(has(self.export) && has(self.export.container) && size(self.export.container) > 0) || size(self.secretRef) > 0 || (has(self.export) && has(self.export.useServiceAccountAuth) && self.export.useServiceAccountAuth)",message="secretRef is required when export.container is set (unless useServiceAccountAuth is true)"
Comment on lines +51 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'PushSecretRef|SecretRef' internal/controller/imagebuild/controller.go -C2

Repository: centos-automotive-suite/automotive-dev-operator

Length of output: 3360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- api/v1alpha1/imagebuild_types.go (relevant section) ---'
sed -n '1,220p' api/v1alpha1/imagebuild_types.go | cat -n | sed -n '1,220p'

echo
echo '--- controller push/auth paths ---'
sed -n '1580,1705p' internal/controller/imagebuild/controller.go | cat -n | sed -n '1,180p'

echo
echo '--- all PushSecretRef/SecretRef references in api and controller ---'
rg -n 'PushSecretRef|SecretRef|useServiceAccountAuth' api/v1alpha1 internal/controller/imagebuild -C 2

Repository: centos-automotive-suite/automotive-dev-operator

Length of output: 35606


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- export-related controller paths ---'
rg -n 'export\.container|export\.disk\.oci|HasDiskExport|GetLegacyExportURL|GetPushSecretRef|SecretRef' internal/controller/imagebuild/controller.go -C 3

echo
echo '--- export-related helpers in api/v1alpha1/imagebuild_types.go ---'
rg -n 'HasDiskExport|GetLegacyExportURL|GetPushSecretRef|UseServiceAccountAuth|Export' api/v1alpha1/imagebuild_types.go -C 3

Repository: centos-automotive-suite/automotive-dev-operator

Length of output: 10818


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- controller references to container export and push secret wiring ---'
rg -n 'GetContainerPush|PushSecretRef|SecretRef|export\.container|export\.disk\.oci|secret-ref|registry-auth' internal/controller/imagebuild/controller.go -C 4

echo
echo '--- relevant helper methods around export/container/push ---'
sed -n '450,540p' api/v1alpha1/imagebuild_types.go | cat -n

Repository: centos-automotive-suite/automotive-dev-operator

Length of output: 9784


Use pushSecretRef here, not secretRef. The push path passes ImageBuildSpec.GetPushSecretRef() into the registry task, while secretRef is wired separately for registry-auth/flash OCI auth. These rules will reject specs that set only pushSecretRef for export.container or export.disk.oci, and they don’t enforce the credential the push flow actually uses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/v1alpha1/imagebuild_types.go` around lines 51 - 52, Update the
XValidation rules on ImageBuildSpec for export.container and export.disk.oci to
require pushSecretRef, not secretRef, unless useServiceAccountAuth is enabled.
Keep the existing export presence checks and exception behavior, and align the
validation with the credential consumed by ImageBuildSpec.GetPushSecretRef().

type ImageBuildSpec struct {
// ─── Common fields ───

Expand Down Expand Up @@ -218,8 +220,9 @@ type AIBSpec struct {

// ExportSpec defines the configuration for exporting build artifacts
type ExportSpec struct {
// Format specifies the disk image output format (e.g., raw, qcow2, simg, or any AIB-supported format)
// +kubebuilder:default=qcow2
// Format specifies the disk image output format (e.g., raw, qcow2, simg, or any AIB-supported format).
// When omitted, the controller resolves the format from the aib-target-defaults ConfigMap,
// falling back to qcow2 if no target default is configured.
Format string `json:"format,omitempty"`

// Compression specifies the compression algorithm for artifacts
Expand Down Expand Up @@ -313,6 +316,12 @@ type ImageBuildStatus struct {
// Used to determine whether an expired build originally succeeded or failed.
// +optional
PreviousPhase string `json:"previousPhase,omitempty"`

// ResolvedExportFormat is the export format resolved at build creation time.
// Persisted so the push task uses the same format even if the
// aib-target-defaults ConfigMap changes between build and push.
// +optional
ResolvedExportFormat string `json:"resolvedExportFormat,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
13 changes: 7 additions & 6 deletions api/v1alpha1/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package v1alpha1
// Observability label and annotation keys.
// LabelDistro, LabelTarget, LabelArchitecture are defined in catalogimage_types.go.
const (
LabelBuildMode = "automotive.sdv.cloud.redhat.com/build-mode"
LabelTraceID = "automotive.sdv.cloud.redhat.com/trace-id"
LabelImageBuildName = "automotive.sdv.cloud.redhat.com/imagebuild-name"
LabelTaskType = "automotive.sdv.cloud.redhat.com/task-type"
LabelWorkspaceName = "automotive.sdv.cloud.redhat.com/workspace-name"
LabelOwner = "automotive.sdv.cloud.redhat.com/owner"
LabelBuildMode = "automotive.sdv.cloud.redhat.com/build-mode"
LabelTraceID = "automotive.sdv.cloud.redhat.com/trace-id"
LabelImageBuildName = "automotive.sdv.cloud.redhat.com/imagebuild-name"
LabelTaskType = "automotive.sdv.cloud.redhat.com/task-type"
LabelWorkspaceName = "automotive.sdv.cloud.redhat.com/workspace-name"
LabelScheduledImageBuildName = "automotive.sdv.cloud.redhat.com/scheduledimagebuild-name"
LabelOwner = "automotive.sdv.cloud.redhat.com/owner"

AnnotationTraceID = "automotive.sdv.cloud.redhat.com/trace-id"
AnnotationRequestedBy = "automotive.sdv.cloud.redhat.com/requested-by"
Expand Down
220 changes: 220 additions & 0 deletions api/v1alpha1/scheduledimagebuild_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/*
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 (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ConcurrencyPolicy describes how the schedule treats overlapping builds.
// +kubebuilder:validation:Enum=Allow;Forbid;Replace
type ConcurrencyPolicy string

// ConcurrencyPolicy values.
const (
AllowConcurrent ConcurrencyPolicy = "Allow"
ForbidConcurrent ConcurrencyPolicy = "Forbid"
ReplaceConcurrent ConcurrencyPolicy = "Replace"
)

// ScheduledImageBuildSpec defines the desired state of ScheduledImageBuild
// +kubebuilder:validation:XValidation:rule="!has(self.matrix) || !has(self.matrix.distros) || size(self.matrix.distros) == 0 || has(self.imageBuildTemplate.spec.aib)",message="matrix distros requires aib in imageBuildTemplate"
// +kubebuilder:validation:XValidation:rule="!has(self.matrix) || !has(self.matrix.targets) || size(self.matrix.targets) == 0 || has(self.imageBuildTemplate.spec.aib)",message="matrix targets requires aib in imageBuildTemplate"
type ScheduledImageBuildSpec struct {
// Schedule is a cron expression defining when builds should run (5-field standard format).
// Examples: "0 2 * * *" (daily at 2am), "0 */6 * * *" (every 6 hours)
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=9
// +kubebuilder:validation:Pattern=`^([-0-9*/,]+\s+){4}[-0-9*/,]+$`
Schedule string `json:"schedule"`

// Suspend tells the controller to suspend subsequent executions.
// Existing running builds will not be affected.
// +optional
Suspend *bool `json:"suspend,omitempty"`

// ConcurrencyPolicy specifies how to treat concurrent builds.
// +kubebuilder:default=Forbid
// +optional
ConcurrencyPolicy ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"`

// StartingDeadlineSeconds is the deadline in seconds for starting a build
// if it misses its scheduled time. Missed builds beyond this window are skipped.
// +kubebuilder:validation:Minimum=0
// +optional
StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"`
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// SuccessfulBuildsHistoryLimit is the number of successful finished builds to retain.
// +kubebuilder:default=3
// +kubebuilder:validation:Minimum=0
// +optional
SuccessfulBuildsHistoryLimit *int32 `json:"successfulBuildsHistoryLimit,omitempty"`

// FailedBuildsHistoryLimit is the number of failed finished builds to retain.
// +kubebuilder:default=1
// +kubebuilder:validation:Minimum=0
// +optional
FailedBuildsHistoryLimit *int32 `json:"failedBuildsHistoryLimit,omitempty"`

// ImageBuildTemplate is the template for creating ImageBuild CRs.
// +kubebuilder:validation:Required
ImageBuildTemplate ImageBuildTemplateSpec `json:"imageBuildTemplate"`

// Matrix defines a build matrix that creates multiple ImageBuilds per schedule tick.
// Each tick creates one ImageBuild for each combination of the specified dimensions,
// overriding the corresponding fields in the imageBuildTemplate.
// +optional
Matrix *BuildMatrix `json:"matrix,omitempty"`

// PublishToCatalog configures automatic publishing of completed builds to the catalog.
// +optional
PublishToCatalog *PublishToCatalogSpec `json:"publishToCatalog,omitempty"`
}

// ImageBuildTemplateSpec describes the ImageBuild that will be created on each schedule tick.
type ImageBuildTemplateSpec struct {
// Metadata contains labels and annotations to apply to created ImageBuilds.
// +optional
Metadata ScheduledBuildMetadata `json:"metadata,omitempty"`

// Spec is the ImageBuildSpec used as the template for child ImageBuilds.
// +kubebuilder:validation:Required
Spec ImageBuildSpec `json:"spec"`
}

// ScheduledBuildMetadata contains metadata to apply to child ImageBuilds.
type ScheduledBuildMetadata struct {
// Labels to set on created ImageBuilds.
// +optional
Labels map[string]string `json:"labels,omitempty"`

// Annotations to set on created ImageBuilds.
// +optional
Annotations map[string]string `json:"annotations,omitempty"`
}

// BuildMatrix defines multiple configurations to build on each schedule tick.
// Each dimension list overrides the corresponding scalar field in the template spec.
// The cross-product of all dimensions determines how many ImageBuilds are created per tick.
type BuildMatrix struct {
// Architectures lists target architectures to build for.
// Each value overrides imageBuildTemplate.spec.architecture.
// +optional
// +kubebuilder:validation:MaxItems=4
Architectures []string `json:"architectures,omitempty"`

// Distros lists distributions to build for.
// Each value overrides imageBuildTemplate.spec.aib.distro.
// +optional
// +kubebuilder:validation:MaxItems=4
Distros []string `json:"distros,omitempty"`

// Targets lists hardware targets to build for.
// Each value overrides imageBuildTemplate.spec.aib.target.
// +optional
// +kubebuilder:validation:MaxItems=4
Targets []string `json:"targets,omitempty"`
}

// PublishToCatalogSpec configures automatic catalog publishing for completed builds.
type PublishToCatalogSpec struct {
// Enabled controls whether completed builds are automatically published to the catalog.
Enabled bool `json:"enabled"`

// Tags are category tags to apply to the CatalogImage.
// +optional
Tags []string `json:"tags,omitempty"`

// AuthSecretRef references a secret containing registry credentials
// for verifying the published image.
// +optional
AuthSecretRef *AuthSecretReference `json:"authSecretRef,omitempty"`
}

// ScheduledImageBuildPhase represents the current state of the schedule.
// +kubebuilder:validation:Enum=Active;Suspended
type ScheduledImageBuildPhase string

// ScheduledImageBuildPhase values.
const (
ScheduledImageBuildPhaseActive ScheduledImageBuildPhase = "Active"
ScheduledImageBuildPhaseSuspended ScheduledImageBuildPhase = "Suspended"
)

// ScheduledImageBuildStatus defines the observed state of ScheduledImageBuild
type ScheduledImageBuildStatus struct {
// ObservedGeneration is the most recent generation observed by the controller.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`

// Phase represents the current state of the schedule.
// +optional
Phase ScheduledImageBuildPhase `json:"phase,omitempty"`

// LastScheduleTime is when the last build was created.
// +optional
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`

// LastSuccessfulTime is when the last build completed successfully.
// +optional
LastSuccessfulTime *metav1.Time `json:"lastSuccessfulTime,omitempty"`

// LastFailedTime is when the last build failed.
// +optional
LastFailedTime *metav1.Time `json:"lastFailedTime,omitempty"`

// Active is a list of currently running ImageBuild references.
// +optional
Active []corev1.ObjectReference `json:"active,omitempty"`

// Conditions represent the latest available observations.
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Schedule",type=string,JSONPath=`.spec.schedule`,priority=0
// +kubebuilder:printcolumn:name="Suspend",type=boolean,JSONPath=`.spec.suspend`,priority=0
// +kubebuilder:printcolumn:name="Last Schedule",type=date,JSONPath=`.status.lastScheduleTime`,priority=0
// +kubebuilder:printcolumn:name="Last Success",type=date,JSONPath=`.status.lastSuccessfulTime`,priority=0
// +kubebuilder:printcolumn:name="Last Failure",type=date,JSONPath=`.status.lastFailedTime`,priority=0
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,priority=0

// ScheduledImageBuild defines a cron schedule for creating ImageBuild CRs
// with optional automatic publishing to the catalog.
type ScheduledImageBuild struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec ScheduledImageBuildSpec `json:"spec,omitempty"`
Status ScheduledImageBuildStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// ScheduledImageBuildList contains a list of ScheduledImageBuild
type ScheduledImageBuildList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ScheduledImageBuild `json:"items"`
}

func init() {
SchemeBuilder.Register(&ScheduledImageBuild{}, &ScheduledImageBuildList{})
}
44 changes: 44 additions & 0 deletions api/v1alpha1/scheduledimagebuild_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package v1alpha1

import (
"regexp"
"testing"
)

// cronPattern mirrors the Pattern marker on ScheduledImageBuildSpec.Schedule.
var cronPattern = regexp.MustCompile(`^([-0-9*/,]+\s+){4}[-0-9*/,]+$`)

func TestScheduleCronPattern(t *testing.T) {
tests := []struct {
name string
input string
isValid bool
}{
// valid expressions
{name: "daily at 2am", input: "0 2 * * *", isValid: true},
{name: "every 6 hours", input: "0 */6 * * *", isValid: true},
{name: "weekdays at midnight", input: "0 0 * * 1-5", isValid: true},
{name: "every 15 minutes", input: "*/15 * * * *", isValid: true},
{name: "specific day and time", input: "30 4 1,15 * *", isValid: true},
{name: "complex range", input: "0 0-6/2 * * 0,6", isValid: true},
{name: "all wildcards", input: "* * * * *", isValid: true},
// invalid expressions
{name: "text input", input: "every tuesday", isValid: false},
{name: "only 3 fields", input: "* * *", isValid: false},
{name: "only 4 fields", input: "0 2 * *", isValid: false},
{name: "6 fields", input: "0 2 * * * *", isValid: false},
{name: "empty string", input: "", isValid: false},
{name: "letters mixed", input: "0 2 * jan *", isValid: false},
{name: "at-syntax", input: "@daily", isValid: false},
{name: "natural language", input: "run at 2am", isValid: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := cronPattern.MatchString(tt.input)
if got != tt.isValid {
t.Errorf("cronPattern.MatchString(%q) = %v, want %v", tt.input, got, tt.isValid)
}
})
}
}
Loading
Loading