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
16 changes: 16 additions & 0 deletions api/v1alpha1/operatorconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ const (
// DefaultFlashLeaseTags is the fallback lease tags when none configured in OperatorConfig
DefaultFlashLeaseTags = "platform=caib"

// DefaultOrasImage is the default ORAS CLI image mounted as an OCI volume
DefaultOrasImage = "ghcr.io/oras-project/oras:v1.2.0"

// DefaultToolchainImage is the default container image for workspace toolchains
DefaultToolchainImage = "quay.io/rh-sdv-cloud/autosd-toolchain:latest"

Expand Down Expand Up @@ -97,6 +100,11 @@ type ImagesConfig struct {
// Operator is the operator container image (overridden by OPERATOR_IMAGE env var when set)
// +optional
Operator string `json:"operator,omitempty"`

// Oras is the ORAS CLI image mounted as an OCI volume in build pods.
// Only used when the OCIVolumes feature gate is enabled.
// +optional
Oras string `json:"oras,omitempty"`
}

// GetAutomotiveImageBuilderImage returns the AIB image, falling back to the default
Expand Down Expand Up @@ -131,6 +139,14 @@ func (c *ImagesConfig) GetOperatorImage() string {
return DefaultOperatorImage
}

// GetOrasImage returns the ORAS CLI image, falling back to the default
func (c *ImagesConfig) GetOrasImage() string {
if c != nil && c.Oras != "" {
return c.Oras
}
return DefaultOrasImage
}

// BuildAPIResourcesConfig defines resource requirements for Build API components
type BuildAPIResourcesConfig struct {
// BuildAPI defines resource requirements for the Build API container
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,11 @@ spec:
description: Operator is the operator container image (overridden
by OPERATOR_IMAGE env var when set)
type: string
oras:
description: |-
Oras is the ORAS CLI image mounted as an OCI volume in build pods.
Only used when the OCIVolumes feature gate is enabled.
type: string
yqHelper:
description: YQHelper is the yq helper image used in Tekton task
steps
Expand Down
169 changes: 169 additions & 0 deletions internal/common/tasks/oci_volumes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package tasks

import (
"testing"

corev1 "k8s.io/api/core/v1"
)

const testOrasImage = "ghcr.io/oras-project/oras:v1.2.0"

func TestOCIVolumes_MountsOnBuildTask(t *testing.T) {
cfg := &BuildConfig{
UseOCIVolumes: true,
OrasImage: testOrasImage,
}
task := GenerateBuildAutomotiveImageTask("test-ns", cfg, "")

assertTaskHasNoVolume(t, task.Spec.Volumes)
for _, step := range task.Spec.Steps {
assertHasVolumeMount(t, step.VolumeMounts, ociVolumeNameOras, ociMountPathOras)
}
}

func TestOCIVolumes_ReturnedByOCIVolumes(t *testing.T) {
cfg := &BuildConfig{
UseOCIVolumes: true,
OrasImage: testOrasImage,
}

vols := OCIVolumes(cfg)
assertHasImageVolume(t, vols, ociVolumeNameOras, testOrasImage)
}

func TestOCIVolumes_AbsentWhenDisabled(t *testing.T) {
cfg := &BuildConfig{
UseOCIVolumes: false,
}
task := GenerateBuildAutomotiveImageTask("test-ns", cfg, "")

assertTaskHasNoVolume(t, task.Spec.Volumes)
if vols := OCIVolumes(cfg); len(vols) != 0 {
t.Fatal("OCIVolumes should return nil when disabled")
}
}

func TestOCIVolumes_AbsentWhenNilConfig(t *testing.T) {
task := GenerateBuildAutomotiveImageTask("test-ns", nil, "")

assertTaskHasNoVolume(t, task.Spec.Volumes)
if vols := OCIVolumes(nil); len(vols) != 0 {
t.Fatal("OCIVolumes should return nil with nil config")
}
}

func TestOCIVolumes_AbsentWhenEmptyImage(t *testing.T) {
cfg := &BuildConfig{
UseOCIVolumes: true,
OrasImage: "",
}
task := GenerateBuildAutomotiveImageTask("test-ns", cfg, "")

assertTaskHasNoVolume(t, task.Spec.Volumes)
if vols := OCIVolumes(cfg); len(vols) != 0 {
t.Fatal("OCIVolumes should return nil when OrasImage is empty")
}
}

func TestOCIVolumes_MountsOnPushTask(t *testing.T) {
cfg := &BuildConfig{
UseOCIVolumes: true,
OrasImage: testOrasImage,
}
task := GeneratePushArtifactRegistryTask("test-ns", cfg)

assertTaskHasNoVolume(t, task.Spec.Volumes)
for _, step := range task.Spec.Steps {
assertHasVolumeMount(t, step.VolumeMounts, ociVolumeNameOras, ociMountPathOras)
}
}

func TestOCIVolumes_AbsentOnSealedTask(t *testing.T) {
cfg := &BuildConfig{
UseOCIVolumes: true,
OrasImage: testOrasImage,
}
task := GenerateSealedTaskForOperation("test-ns", "prepare", cfg)

assertTaskHasNoVolume(t, task.Spec.Volumes)
for _, step := range task.Spec.Steps {
for _, vm := range step.VolumeMounts {
if vm.Name == ociVolumeNameOras {
t.Fatalf("sealed task step %q should not have OCI volume mount", step.Name)
}
}
}
}

func TestOCIVolumes_ReadOnly(t *testing.T) {
cfg := &BuildConfig{
UseOCIVolumes: true,
OrasImage: testOrasImage,
}
task := GenerateBuildAutomotiveImageTask("test-ns", cfg, "")

for _, step := range task.Spec.Steps {
for _, vm := range step.VolumeMounts {
if vm.Name == ociVolumeNameOras && !vm.ReadOnly {
t.Fatalf("ORAS volume mount on step %q should be read-only", step.Name)
}
}
}
}

func TestOCIVolumes_PullPolicy(t *testing.T) {
cfg := &BuildConfig{
UseOCIVolumes: true,
OrasImage: testOrasImage,
}

vols := OCIVolumes(cfg)
for _, vol := range vols {
if vol.Name == ociVolumeNameOras {
if vol.Image == nil {
t.Fatal("ORAS volume should use Image volume source")
}
if vol.Image.PullPolicy != corev1.PullIfNotPresent {
t.Fatalf("ORAS volume pull policy = %v, want IfNotPresent", vol.Image.PullPolicy)
}
}
}
}

func assertHasImageVolume(t *testing.T, volumes []corev1.Volume, name, reference string) {
t.Helper()
for _, vol := range volumes {
if vol.Name == name {
if vol.Image == nil {
t.Fatalf("volume %q exists but is not an Image volume", name)
}
if vol.Image.Reference != reference {
t.Fatalf("volume %q reference = %q, want %q", name, vol.Image.Reference, reference)
}
return
}
}
t.Fatalf("image volume %q not found", name)
}

func assertTaskHasNoVolume(t *testing.T, volumes []corev1.Volume) {
t.Helper()
for _, vol := range volumes {
if vol.Name == ociVolumeNameOras {
t.Fatalf("volume %q should not be on Task spec (belongs in podTemplate)", vol.Name)
}
}
}

func assertHasVolumeMount(t *testing.T, mounts []corev1.VolumeMount, name, path string) {
t.Helper()
for _, vm := range mounts {
if vm.Name == name {
if vm.MountPath != path {
t.Fatalf("volume mount %q path = %q, want %q", name, vm.MountPath, path)
}
return
}
}
t.Fatalf("volume mount %q not found", name)
}
26 changes: 1 addition & 25 deletions internal/common/tasks/scripts/build_image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,31 +92,7 @@ RESTORE_SOURCES_REF="$(params.restore-sources-ref)"
if [ -n "$RESTORE_SOURCES_REF" ]; then
echo "=== Restoring sources from $RESTORE_SOURCES_REF ==="

ORAS_VERSION="1.2.0"
case "$(uname -m)" in
x86_64) ORAS_ARCH="amd64" ;;
aarch64|arm64) ORAS_ARCH="arm64" ;;
*) echo "ERROR: Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
ORAS_TARBALL="oras_${ORAS_VERSION}_linux_${ORAS_ARCH}.tar.gz"
ORAS_BASE_URL="https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}"
ORAS_CHECKSUMS="oras_${ORAS_VERSION}_checksums.txt"
curl -sLO "${ORAS_BASE_URL}/${ORAS_TARBALL}"
curl -sLO "${ORAS_BASE_URL}/${ORAS_CHECKSUMS}"
expected_checksum=$(grep "${ORAS_TARBALL}" "${ORAS_CHECKSUMS}" | cut -d' ' -f1)
if command -v sha256sum >/dev/null; then
actual_checksum=$(sha256sum "${ORAS_TARBALL}" | cut -d' ' -f1)
else
actual_checksum=$(shasum -a 256 "${ORAS_TARBALL}" | cut -d' ' -f1)
fi
if [ "$expected_checksum" != "$actual_checksum" ]; then
echo "ERROR: ORAS checksum verification failed" >&2; exit 1
fi
tar -zxf "$ORAS_TARBALL" oras
mkdir -p "$HOME/bin"
mv oras "$HOME/bin/"
rm -f "$ORAS_TARBALL" "$ORAS_CHECKSUMS"
export PATH="$HOME/bin:$PATH"
install_oras || exit 1

ORAS_AUTH_FLAGS=()
if [ -n "$REGISTRY_AUTH_FILE" ] && [ -f "$REGISTRY_AUTH_FILE" ]; then
Expand Down
103 changes: 103 additions & 0 deletions internal/common/tasks/scripts/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,109 @@ if [[ -n "${ADO_TRACE_ID:-}" ]]; then
echo "{\"traceID\":\"${ADO_TRACE_ID}\",\"msg\":\"task started\",\"hostname\":\"${HOSTNAME:-unknown}\"}"
fi

# Add OCI volume tool paths when mounted (OCIVolumes feature gate)
OCI_TOOLS_BASE="/oci-tools"
if [ -d "$OCI_TOOLS_BASE/oras/bin" ]; then
export PATH="$OCI_TOOLS_BASE/oras/bin:$PATH"
fi

Comment thread
bennyz marked this conversation as resolved.
# --- ORAS install ---

# install_oras downloads and checksum-verifies the ORAS CLI binary.
# Sets ORAS_BIN to the installed path and adds it to PATH.
# Skips if oras is already available (e.g. via OCI volume mount).
install_oras() {
if command -v oras >/dev/null 2>&1; then
ORAS_BIN="$(command -v oras)"
echo "ORAS already available at $ORAS_BIN"
return 0
fi
Comment on lines +41 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the oras binary location change, then command -v oras fails silently, install_oras falls back to downloading, and the feature appears to work but the OCI volume goes unused with no warning emitted.
I think adding a log line is much helpful when the PATH directory doesn't exist
echo "WARN: OCI volume mounted but ORAS binary not found at expected path, falling back to download" >&2


if [ -d "$OCI_TOOLS_BASE/oras" ]; then
echo "WARN: OCI volume mounted at $OCI_TOOLS_BASE/oras but oras binary not found on PATH, falling back to download" >&2
fi

ORAS_VERSION="1.2.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

oras version is hardcoded in the fallback. If the operator configure spec.images.oras: ghcr.io/oras-project/oras:v1.3.0 then OCI volume has v1.3.0 but the fallback download still installs v1.2.0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we can introduce a variable to the task, but the plan is that over time (not too long hopefully) only ImageVolume will be used and the fallback will be removed entirely

case "$(uname -m)" in
x86_64) ORAS_ARCH="amd64" ;;
aarch64|arm64) ORAS_ARCH="arm64" ;;
*)
echo "ERROR: Unsupported architecture: $(uname -m)" >&2
return 1
;;
esac
ORAS_TARBALL="oras_${ORAS_VERSION}_linux_${ORAS_ARCH}.tar.gz"
ORAS_BASE_URL="https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}"
ORAS_CHECKSUMS="oras_${ORAS_VERSION}_checksums.txt"

_cleanup_oras_files() {
rm -f "$ORAS_TARBALL" "$ORAS_CHECKSUMS" oras
}

trap _cleanup_oras_files EXIT

echo "Downloading ORAS ${ORAS_VERSION} with integrity verification..."

curl -LO "${ORAS_BASE_URL}/${ORAS_TARBALL}" || {
echo "ERROR: Failed to download ORAS tarball" >&2
return 1
}

curl -LO "${ORAS_BASE_URL}/${ORAS_CHECKSUMS}" || {
echo "ERROR: Failed to download ORAS checksums" >&2
return 1
}

expected_checksum=$(grep "${ORAS_TARBALL}" "${ORAS_CHECKSUMS}" | cut -d' ' -f1)
if [ -z "$expected_checksum" ]; then
echo "ERROR: Could not find checksum for ${ORAS_TARBALL} in checksums file" >&2
return 1
fi

if command -v sha256sum >/dev/null; then
actual_checksum=$(sha256sum "${ORAS_TARBALL}" | cut -d' ' -f1)
elif command -v shasum >/dev/null; then
actual_checksum=$(shasum -a 256 "${ORAS_TARBALL}" | cut -d' ' -f1)
else
echo "ERROR: Neither sha256sum nor shasum available for checksum verification" >&2
return 1
fi

if [ "$expected_checksum" != "$actual_checksum" ]; then
echo "ERROR: Checksum verification failed for ${ORAS_TARBALL}" >&2
echo " Expected: $expected_checksum" >&2
echo " Actual: $actual_checksum" >&2
return 1
fi

echo "Checksum verification passed: $expected_checksum"

tar -zxf "$ORAS_TARBALL" oras || {
echo "ERROR: Failed to extract ORAS from tarball" >&2
return 1
}

ORAS_INSTALL_DIR="${HOME:-/tmp}/bin"
if [ "$ORAS_INSTALL_DIR" = "//bin" ] || [ "$ORAS_INSTALL_DIR" = "/bin" ]; then
ORAS_INSTALL_DIR="/tmp/bin"
fi
mkdir -p "$ORAS_INSTALL_DIR"
mv oras "$ORAS_INSTALL_DIR/" || {
echo "ERROR: Failed to install ORAS binary" >&2
return 1
}

if ! echo "$PATH" | grep -q "$ORAS_INSTALL_DIR"; then
export PATH="$ORAS_INSTALL_DIR:$PATH"
fi

_cleanup_oras_files
trap - EXIT

ORAS_BIN="$ORAS_INSTALL_DIR/oras"
echo "ORAS ${ORAS_VERSION} installed successfully"
}

# --- Validation ---

validate_container_ref() {
Expand Down
Loading
Loading