Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
12 changes: 7 additions & 5 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ require "standard/rake"

namespace :stemcell do
desc "Build a base OS image for use in stemcells"
task :build_os_image, [:operating_system_name, :operating_system_version, :os_image_path] do |_, args|
task :build_os_image, [:operating_system_name, :operating_system_version, :os_image_path, :architecture] do |_, args|
require "bosh/stemcell/archive_handler"
require "bosh/stemcell/build_environment"
require "bosh/stemcell/definition"
require "bosh/stemcell/os_image_builder"
require "bosh/stemcell/stage_collection"
require "bosh/stemcell/stage_runner"

definition = Bosh::Stemcell::Definition.for("null", "null", args.operating_system_name, args.operating_system_version)
args.with_defaults(architecture: Bosh::Stemcell::Definition::DEFAULT_ARCHITECTURE)

definition = Bosh::Stemcell::Definition.for("null", "null", args.operating_system_name, args.operating_system_version, args.architecture)
environment = Bosh::Stemcell::BuildEnvironment.new(
ENV.to_hash,
definition,
Expand Down Expand Up @@ -42,17 +44,17 @@ namespace :stemcell do
end

desc "Build a stemcell, requires `os_image_path` pointing at an image created via `stemcell:build_os_image`"
task :build, [:infrastructure_name, :hypervisor_name, :operating_system_name, :operating_system_version, :os_image_path, :build_number] do |_, args|
task :build, [:infrastructure_name, :hypervisor_name, :operating_system_name, :operating_system_version, :os_image_path, :build_number, :architecture] do |_, args|
require "bosh/stemcell/build_environment"
require "bosh/stemcell/definition"
require "bosh/stemcell/stage_collection"
require "bosh/stemcell/stage_runner"
require "bosh/stemcell/stemcell_packager"
require "bosh/stemcell/stemcell_builder"

args.with_defaults(build_number: "0000")
args.with_defaults(build_number: "0000", architecture: Bosh::Stemcell::Definition::DEFAULT_ARCHITECTURE)

definition = Bosh::Stemcell::Definition.for(args.infrastructure_name, args.hypervisor_name, args.operating_system_name, args.operating_system_version)
definition = Bosh::Stemcell::Definition.for(args.infrastructure_name, args.hypervisor_name, args.operating_system_name, args.operating_system_version, args.architecture)
environment = Bosh::Stemcell::BuildEnvironment.new(
ENV.to_hash,
definition,
Expand Down
1 change: 1 addition & 0 deletions bosh-stemcell/lib/bosh/stemcell/builder_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def default
"stemcell_operating_system" => operating_system.name,
"stemcell_operating_system_version" => operating_system.version,
"stemcell_operating_system_variant" => operating_system.variant,
"stemcell_arch" => definition.architecture,
"ruby_bin" => ruby_bin,
"image_create_disk_size" => image_create_disk_size,
"os_image_tgz" => os_image_tgz_path
Expand Down
19 changes: 14 additions & 5 deletions bosh-stemcell/lib/bosh/stemcell/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@

module Bosh::Stemcell
class Definition
attr_reader :infrastructure, :hypervisor_name, :operating_system
# Architecture defaults to amd64 so existing stemcell builds are unaffected.
DEFAULT_ARCHITECTURE = "amd64".freeze

def self.for(infrastructure_name, hypervisor_name, operating_system_name, operating_system_version)
attr_reader :infrastructure, :hypervisor_name, :operating_system, :architecture

def self.for(infrastructure_name, hypervisor_name, operating_system_name, operating_system_version, architecture = DEFAULT_ARCHITECTURE)
new(
Bosh::Stemcell::Infrastructure.for(infrastructure_name),
hypervisor_name,
Bosh::Stemcell::OperatingSystem.for(operating_system_name, operating_system_version)
Bosh::Stemcell::OperatingSystem.for(operating_system_name, operating_system_version),
architecture
)
end

def initialize(infrastructure, hypervisor_name, operating_system)
def initialize(infrastructure, hypervisor_name, operating_system, architecture = DEFAULT_ARCHITECTURE)
@infrastructure = infrastructure
@hypervisor_name = hypervisor_name
@operating_system = operating_system
@architecture = architecture || DEFAULT_ARCHITECTURE
end

def stemcell_name(disk_format)
Expand All @@ -28,6 +33,9 @@ def stemcell_name(disk_format)
stemcell_name_parts << operating_system.version if operating_system.version
stemcell_name_parts << operating_system.variant if operating_system.variant
stemcell_name_parts << disk_format unless disk_format == infrastructure.default_disk_format
# Only embed the architecture in the name for non-default (arm64) builds so
# existing amd64 stemcell names remain byte-identical.
stemcell_name_parts << architecture unless architecture == DEFAULT_ARCHITECTURE

stemcell_name_parts.join("-")
end
Expand All @@ -38,7 +46,8 @@ def disk_formats

def ==(other)
infrastructure == other.infrastructure &&
operating_system == other.operating_system
operating_system == other.operating_system &&
architecture == other.architecture
end
end
end
4 changes: 3 additions & 1 deletion bosh-stemcell/lib/bosh/stemcell/stemcell_packager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ def manifest(disk_format)
end

def manifest_cloud_properties(disk_format, infrastructure, stemcell_name)
architecture = "x86_64"
# Map the BOSH/Debian architecture to the kernel architecture string
# recorded in the manifest. Defaults to x86_64 for amd64 builds.
architecture = {"amd64" => "x86_64", "arm64" => "aarch64"}.fetch(definition.architecture, "x86_64")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

{
"name" => stemcell_name,
Expand Down
8 changes: 7 additions & 1 deletion bosh-stemcell/spec/bosh/stemcell/builder_options_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ module Bosh::Stemcell
instance_double(
"Bosh::Stemcell::Definition",
infrastructure: infrastructure,
operating_system: operating_system
operating_system: operating_system,
architecture: "amd64"
)
}

Expand Down Expand Up @@ -58,6 +59,11 @@ module Bosh::Stemcell
expect(result["stemcell_operating_system_variant"]).to eq("bear")
end

it "sets the stemcell architecture" do
result = stemcell_builder_options.default
expect(result["stemcell_arch"]).to eq("amd64")
end

# rubocop:disable Metrics/MethodLength
def self.it_sets_correct_environment_variables
describe "setting enviroment variables" do
Expand Down
62 changes: 61 additions & 1 deletion bosh-stemcell/spec/bosh/stemcell/definition_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module Bosh::Stemcell
definition = instance_double("Bosh::Stemcell::Definition")
expect(Bosh::Stemcell::Definition)
.to receive(:new)
.with(infrastructure, hypervisor, operating_system)
.with(infrastructure, hypervisor, operating_system, "amd64")
.and_return(definition)

Bosh::Stemcell::Definition.for(
Expand All @@ -51,12 +51,52 @@ module Bosh::Stemcell
"operating-system-version"
)
end

it "passes an explicit architecture through to the new definition" do
expect(Bosh::Stemcell::Infrastructure)
.to receive(:for)
.with("infrastructure-name")
.and_return(infrastructure)

expect(Bosh::Stemcell::OperatingSystem)
.to receive(:for)
.with("operating-system-name", "operating-system-version")
.and_return(operating_system)

definition = instance_double("Bosh::Stemcell::Definition")
expect(Bosh::Stemcell::Definition)
.to receive(:new)
.with(infrastructure, hypervisor, operating_system, "arm64")
.and_return(definition)

Bosh::Stemcell::Definition.for(
"infrastructure-name",
hypervisor,
"operating-system-name",
"operating-system-version",
"arm64"
)
end
end

describe "#initialize" do
its(:infrastructure) { should == infrastructure }
its(:operating_system) { should == operating_system }
its(:hypervisor_name) { should == hypervisor }

it "defaults the architecture to amd64" do
expect(definition.architecture).to eq("amd64")
end

context "when an architecture is provided" do
subject(:definition) do
Bosh::Stemcell::Definition.new(infrastructure, hypervisor, operating_system, "arm64")
end

it "uses the provided architecture" do
expect(definition.architecture).to eq("arm64")
end
end
end

describe "#==" do
Expand Down Expand Up @@ -113,6 +153,26 @@ module Bosh::Stemcell
)
end
end

context "the architecture is the default (amd64)" do
it "leaves the architecture off the name" do
expect(definition.stemcell_name("disk-format")).to eq(
"infrastructure-name-hypervisor-operating-system-name-operating_system_version-disk-format"
)
end
end

context "the architecture is non-default (arm64)" do
subject(:definition) do
Bosh::Stemcell::Definition.new(infrastructure, hypervisor, operating_system, "arm64")
end

it "appends the architecture to the name" do
expect(definition.stemcell_name("disk-format")).to eq(
"infrastructure-name-hypervisor-operating-system-name-operating_system_version-disk-format-arm64"
)
end
end
end

describe "disk_formats" do
Expand Down
28 changes: 20 additions & 8 deletions bosh-stemcell/spec/os_image/ubuntu_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,16 @@

describe "base_apt" do
describe file("/etc/apt/sources.list") do
its(:content) { should match 'deb http:\/\/(archive|snapshot).ubuntu.com\/ubuntu(|\/\d*T\d*Z) noble main universe multiverse' }
its(:content) { should match 'deb http:\/\/(archive|snapshot).ubuntu.com\/ubuntu(|\/\d*T\d*Z) noble-updates main universe multiverse' }
its(:content) { should match 'deb http:\/\/(security|snapshot).ubuntu.com\/ubuntu(|\/\d*T\d*Z) noble-security main universe multiverse' }
if Bosh::Stemcell::Arch.x86_64?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure we gain a lot by adding this conditional here since the regexs are already fairly complex.

Seems possibly simpler to add ports| to (archive|snapshot) and let the matches handle x84 or arm.

... if doing so would be unsafe I'm fine with if / else but it seems more confusing, and possibly harder to correctly update if one of the three changes (ex noble-updates -> noble-fixes would mean changing two lines).

On further reflection maybe we should use a common regex that matches all the entries and only assert on the "listings" => noble ... noble-updates ... noble-security ... portions:

"deb http:\/\/(archive|ports|security|snapshot).ubuntu.com\/ubuntu(|\/\d*T\d*Z) ${listing}"

its(:content) { should match 'deb http:\/\/(archive|snapshot).ubuntu.com\/ubuntu(|\/\d*T\d*Z) noble main universe multiverse' }
its(:content) { should match 'deb http:\/\/(archive|snapshot).ubuntu.com\/ubuntu(|\/\d*T\d*Z) noble-updates main universe multiverse' }
its(:content) { should match 'deb http:\/\/(security|snapshot).ubuntu.com\/ubuntu(|\/\d*T\d*Z) noble-security main universe multiverse' }
else
# ARM64 (and other non-amd64 arches) are served from the ports mirror.
its(:content) { should match 'deb http:\/\/(ports|snapshot).ubuntu.com\/ubuntu(-ports)?(|\/\d*T\d*Z) noble main universe multiverse' }
its(:content) { should match 'deb http:\/\/(ports|snapshot).ubuntu.com\/ubuntu(-ports)?(|\/\d*T\d*Z) noble-updates main universe multiverse' }
its(:content) { should match 'deb http:\/\/(ports|snapshot).ubuntu.com\/ubuntu(-ports)?(|\/\d*T\d*Z) noble-security main universe multiverse' }
end
end

describe file("/lib/systemd/system/monit.service") do
Expand Down Expand Up @@ -85,14 +92,18 @@
end

context "installed by system_grub" do
%w[
grub2
].each do |pkg|
# ARM64 has no "grub2" metapackage; grub-efi-arm64 is the equivalent.
grub_packages = Bosh::Stemcell::Arch.x86_64? ? %w[grub2] : %w[grub-efi-arm64]
grub_packages.each do |pkg|
describe package(pkg) do
it { should be_installed }
end
end
%w[unicode.pf2 menu.lst gfxblacklist.txt].each do |grub_stage|
# gfxblacklist.txt ships with the amd64 grub2 metapackage and is not present
# in the arm64 grub-efi-arm64 package set.
grub_files = %w[unicode.pf2 menu.lst]
grub_files << "gfxblacklist.txt" if Bosh::Stemcell::Arch.x86_64?
grub_files.each do |grub_stage|
describe file("/boot/grub/#{grub_stage}") do
it { should be_file }
end
Expand Down Expand Up @@ -162,7 +173,8 @@
end

context "PAM configuration" do
describe file("/lib/x86_64-linux-gnu/security/pam_pwquality.so") do
pam_multiarch_triplet = Bosh::Stemcell::Arch.x86_64? ? "x86_64-linux-gnu" : "aarch64-linux-gnu"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like Bosh::Stemcell::Arch should be able to return the right value (x86_64 or aarch64) allowing this to be simplified to:

describe file("/lib/#{Bosh::Stemcell::Arch.gnu_arch}-linux-gnu/security/pam_pwquality.so") do

Note: no idea if #gnu_arch is a good name here.

describe file("/lib/#{pam_multiarch_triplet}/security/pam_pwquality.so") do
it { should be_file }
end

Expand Down
7 changes: 5 additions & 2 deletions bosh-stemcell/spec/support/stemcell_shared_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@
it("should be owned by root user (stig: V-38466)") { should be_owned_by("root") }
end

describe file("/lib64") do
it("should be owned by root user (stig: V-38466)") { should be_owned_by("root") }
# /lib64 holds the x86_64 dynamic linker and is absent on arm64, which uses
# /lib/ld-linux-aarch64.so.1. Skip the ownership check when it is missing,
# mirroring the /usr/lib64 check below. RFC: cloudfoundry/community#1530
describe command('if [ -e /lib64 ]; then stat -c "%U" /lib64 ; else echo "root" ; fi') do

@aramprice aramprice Oct 27, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The else echo "root" seems like something that could cause this spec to silently pass when we don't want it to.

Broader Aside

Overall it seems like we need a pattern for running specs for specific ARCH targets rather than smashing conditionals into various parts of many different spec files.

We probably need some coherent pattern for running/selecting specs for a given ARCH. Maybe rspec tags on specific examples, maybe something like Golang's filename_${GOARCH}.go pattern - this would mean splitting specs between generic and ARCH specific files.

I'm not sure what the right pattern is though I don't think we should have ad-hoc conditionals.

its(:stdout) { should eq("root\n") }
end

describe file("/usr/lib") do
Expand Down
48 changes: 41 additions & 7 deletions ci/docker/os-image-stemcell-builder/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# bosh/os-image-stemcell-builder
ARG BASE_IMAGE
FROM $BASE_IMAGE
FROM $BASE_IMAGE AS base

LABEL maintainer="cf-bosh@lists.cloudfoundry.org"

Expand All @@ -13,9 +13,6 @@ ARG RUBY_INSTALL_URL
ARG RUBY_VERSION
ARG GEM_HOME

ARG OVF_TOOL_INSTALLER
ARG OVF_TOOL_INSTALLER_SHA1

# ENV VARs
ENV DEBIAN_FRONTEND="noninteractive"
ENV LANG="en_US.UTF-8"
Expand Down Expand Up @@ -78,7 +75,19 @@ RUN temp_dir="/mnt/tmp" \
RUN mkdir -p /mnt/stemcells \
&& chown -R ubuntu:ubuntu /mnt/stemcells

# VMware's ovftool is used to create vSphere stemcells
# VMware's ovftool is used to create vSphere stemcells. VMware ships no arm64
# Linux ovftool, and vSphere is not an arm64 (Graviton) target, so the ovftool
# bundle is only added and installed on amd64. On arm64 the ADD is skipped
# entirely (a conditional build stage), which keeps the arm64 image buildable
# without an ovftool bundle in the build context.
# RFC: cloudfoundry/community#1530.
#
# TARGETARCH is provided automatically by BuildKit ("amd64" / "arm64").
ARG TARGETARCH

FROM base AS ovftool-amd64
ARG OVF_TOOL_INSTALLER
ARG OVF_TOOL_INSTALLER_SHA1
ADD ${OVF_TOOL_INSTALLER} /tmp/ovftool_installer.bundle
RUN cd /tmp \
&& echo "${OVF_TOOL_INSTALLER_SHA1} /tmp/ovftool_installer.bundle" | sha1sum -c - \
Expand All @@ -87,9 +96,34 @@ RUN cd /tmp \
&& bash ./ovftool_installer.bundle --eulas-agreed \
&& rm -rf ./ovftool_installer.bundle /tmp/vmware-root/ /usr/local/bin/more

# On arm64 the ovftool stage is a no-op passthrough of the base image.
FROM base AS ovftool-arm64

# Select the arch-appropriate ovftool stage and continue the build from it.
FROM ovftool-${TARGETARCH} AS builder

# Build args are per-stage in BuildKit, so re-declare the ones used below.
ARG META4_CLI_URL
ARG META4_VERSION
ARG SYFT_CLI_URL
ARG YQ_CLI_URL
ARG RUBY_INSTALL_URL
ARG RUBY_VERSION
ARG GEM_HOME

# Use the prebuilt meta4 release binary when one exists for this architecture
# (amd64). dpb587/metalink publishes no arm64 binary, so on arm64
# build-docker-args.sh leaves META4_CLI_URL empty and meta4 is built from source
# with the image's Go toolchain instead.
RUN meta4_cli_path="/usr/local/bin/meta4" \
&& curl --show-error -sL "${META4_CLI_URL}" \
> "${meta4_cli_path}" \
&& if [ -n "${META4_CLI_URL}" ]; then \
curl --show-error -sL "${META4_CLI_URL}" > "${meta4_cli_path}"; \
else \
git clone --depth 1 --branch "${META4_VERSION}" \
https://github.com/dpb587/metalink.git /tmp/metalink \
&& ( cd /tmp/metalink && go build -o "${meta4_cli_path}" ./cli ) \
&& rm -rf /tmp/metalink; \
fi \
&& chmod +x "${meta4_cli_path}"

ENV GEM_HOME="${GEM_HOME}" \
Expand Down
Loading
Loading