Skip to content
Open
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
52 changes: 34 additions & 18 deletions docs/user/reference/cli/azldev_image_test.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 48 additions & 22 deletions internal/app/azldev/cmds/image/boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const (
defaultHostname = "azurelinux-vm"
)

// ImageFormat represents a bootable disk image format.
// ImageFormat represents a disk image or container image format.
type ImageFormat string

const (
Expand All @@ -47,12 +47,25 @@ const (
ImageFormatVhd ImageFormat = "vhd"
// ImageFormatVhdx is the Hyper-V virtual hard disk format.
ImageFormatVhdx ImageFormat = "vhdx"
// ImageFormatOCI is an OCI container image tarball.
ImageFormatOCI ImageFormat = "oci"
)

// SupportedImageFormats returns the list of supported bootable image formats in priority order.
// When multiple formats exist, the first match in this order is selected.
func SupportedImageFormats() []string {
return []string{string(ImageFormatRaw), string(ImageFormatQcow2), string(ImageFormatVhdx), string(ImageFormatVhd)}
// AllImageFormats returns all supported image formats in priority order.
func AllImageFormats() []string {
return []string{
string(ImageFormatRaw), string(ImageFormatQcow2),
string(ImageFormatVhdx), string(ImageFormatVhd),
string(ImageFormatOCI),
}
}

// BootableImageFormats returns the subset of image formats that can be booted in a VM.
func BootableImageFormats() []string {
return []string{
string(ImageFormatRaw), string(ImageFormatQcow2),
string(ImageFormatVhdx), string(ImageFormatVhd),
}
}

// Assert that [ImageFormat] implements the [pflag.Value] interface.
Expand All @@ -74,7 +87,7 @@ func (f *ImageFormat) Set(value string) error {
case string(ImageFormatVhdx):
*f = ImageFormatVhdx
default:
return fmt.Errorf("unsupported image format %#q; supported: %v", value, SupportedImageFormats())
return fmt.Errorf("unsupported image format %#q; supported: %v", value, BootableImageFormats())
}

return nil
Expand Down Expand Up @@ -252,7 +265,7 @@ func addBootFlags(cmd *cobra.Command, options *ImageBootOptions) {
// Register shell completions for flags.
_ = cmd.RegisterFlagCompletionFunc("format",
func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return SupportedImageFormats(), cobra.ShellCompDirectiveNoFileComp
return BootableImageFormats(), cobra.ShellCompDirectiveNoFileComp
})
_ = cmd.RegisterFlagCompletionFunc("arch",
func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
Expand Down Expand Up @@ -449,7 +462,9 @@ func resolveDiskSource(env *azldev.Env, options *ImageBootOptions) (imagePath, i
slog.String("format", imageFormat),
)
case options.ImageName != "":
imagePath, imageFormat, err = findBootableImageArtifact(env, options.ImageName, string(options.Format))
imagePath, imageFormat, err = findImageArtifact(
env, options.ImageName, string(options.Format), BootableImageFormats(),
)
if err != nil {
return "", "", err
}
Expand Down Expand Up @@ -570,18 +585,21 @@ func createBootTempDir(env *azldev.Env) (string, error) {
// Most formats have a single extension matching the format name, but vhd accepts
// both .vhd and .vhdfixed since QEMU treats them identically.
func fileExtensionsForFormat(format string) []string {
if format == string(ImageFormatVhd) {
switch format {
case string(ImageFormatVhd):
return []string{"vhd", "vhdfixed"}
case string(ImageFormatOCI):
return []string{"oci.tar.xz", "oci.tar.gz", "oci.tar"}
default:
return []string{format}
}

return []string{format}
}

// findBootableImageArtifact locates a bootable image artifact in the output directory for the
// given image name. If format is specified, only that format is searched. Otherwise, formats
// are searched in priority order (raw, qcow2, vhdx, vhd) and the first match is returned.
func findBootableImageArtifact(
env *azldev.Env, imageName, format string,
// findImageArtifact locates an image artifact in the output directory for the given image
// name. If format is specified, only that format is searched. Otherwise, searchFormats are
// searched in priority order and the first match is returned.
func findImageArtifact(
env *azldev.Env, imageName, format string, searchFormats []string,
) (imagePath, imageFormat string, err error) {
// First validate the image exists in project configuration.
_, err = ResolveImageByName(env, imageName)
Expand All @@ -605,7 +623,7 @@ func findBootableImageArtifact(
}

// Determine which formats to search.
formatsToSearch := SupportedImageFormats()
formatsToSearch := searchFormats
if format != "" {
formatsToSearch = []string{format}
}
Expand Down Expand Up @@ -645,14 +663,23 @@ func findBootableImageArtifact(
}

return "", "", fmt.Errorf(
"no bootable image artifact found in %#q; supported formats: %v",
imageOutputDir, SupportedImageFormats(),
"no image artifact found in %#q; supported formats: %v",
imageOutputDir, searchFormats,
)
}

// InferImageFormat determines the image format from the file extension.
// Returns an error if the extension does not match a supported format.
func InferImageFormat(imagePath string) (string, error) {
lower := strings.ToLower(imagePath)

// Check multi-part extensions first (e.g., ".oci.tar.xz").
for _, ext := range []string{".oci.tar.xz", ".oci.tar.gz", ".oci.tar"} {
if strings.HasSuffix(lower, ext) {
return string(ImageFormatOCI), nil
}
}

ext := strings.ToLower(filepath.Ext(imagePath))
if ext == "" {
return "", fmt.Errorf(
Expand All @@ -668,11 +695,10 @@ func InferImageFormat(imagePath string) (string, error) {
}

// Validate the inferred format is supported.
supported := SupportedImageFormats()
if !lo.Contains(supported, format) {
if !lo.Contains(AllImageFormats(), format) {
return "", fmt.Errorf(
"unsupported image format %#q inferred from %#q; supported formats: %v",
format, imagePath, supported,
format, imagePath, AllImageFormats(),
)
}

Expand Down
16 changes: 14 additions & 2 deletions internal/app/azldev/cmds/image/boot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,22 @@ func TestResolveImageWithAvailableList_Found(t *testing.T) {
assert.Equal(t, "My test image", cfg.Description)
}

func TestSupportedImageFormats(t *testing.T) {
formats := image.SupportedImageFormats()
func TestBootableImageFormats(t *testing.T) {
formats := image.BootableImageFormats()
require.NotEmpty(t, formats)
assert.Contains(t, formats, "raw")
assert.Contains(t, formats, "qcow2")
assert.Contains(t, formats, "vhdx")
assert.Contains(t, formats, "vhd")
assert.NotContains(t, formats, "oci", "OCI is not a bootable format")
}

func TestAllImageFormats(t *testing.T) {
formats := image.AllImageFormats()
require.NotEmpty(t, formats)
assert.Contains(t, formats, "raw")
assert.Contains(t, formats, "qcow2")
assert.Contains(t, formats, "oci")
}

func TestInferImageFormat(t *testing.T) {
Expand All @@ -226,6 +235,9 @@ func TestInferImageFormat(t *testing.T) {
{name: "vhd", path: "/path/to/image.vhd", expected: "vhd"},
{name: "vhdfixed", path: "/path/to/image.vhdfixed", expected: "vhd"},
{name: "vhdx", path: "/path/to/image.vhdx", expected: "vhdx"},
{name: "oci.tar.xz", path: "/path/to/image.oci.tar.xz", expected: "oci"},
{name: "oci.tar.gz", path: "/path/to/image.oci.tar.gz", expected: "oci"},
{name: "oci.tar", path: "/path/to/image.oci.tar", expected: "oci"},
}

for _, test := range tests {
Expand Down
Loading
Loading