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
64 changes: 9 additions & 55 deletions internal/app/azldev/cmds/image/boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/imagebuilder"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/cloudinit"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/imageformat"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/iso"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/qemu"
"github.com/samber/lo"
Expand All @@ -39,26 +40,15 @@ type ImageFormat string

const (
// ImageFormatRaw is the raw disk image format.
ImageFormatRaw ImageFormat = "raw"
ImageFormatRaw ImageFormat = imageformat.Raw
// ImageFormatQcow2 is the QEMU copy-on-write v2 format.
ImageFormatQcow2 ImageFormat = "qcow2"
ImageFormatQcow2 ImageFormat = imageformat.Qcow2
// ImageFormatVhd is the Hyper-V VHD format (QEMU driver: vpc).
ImageFormatVhd ImageFormat = "vhd"
ImageFormatVhd ImageFormat = imageformat.Vhd
// ImageFormatVhdx is the Hyper-V virtual hard disk format.
ImageFormatVhdx ImageFormat = "vhdx"
// ImageFormatOCI is an OCI container image tarball.
ImageFormatOCI ImageFormat = "oci"
ImageFormatVhdx ImageFormat = imageformat.Vhdx
)

// 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{
Expand Down Expand Up @@ -450,13 +440,14 @@ func resolveDiskSource(env *azldev.Env, options *ImageBootOptions) (imagePath, i
case options.ImagePath != "":
imageFormat = string(options.Format)
if imageFormat == "" {
imageFormat, err = InferImageFormat(options.ImagePath)
imageFormat, err = imageformat.Infer(options.ImagePath)
if err != nil {
//nolint:wrapcheck // Preserve the existing image-format diagnostic unchanged.
return "", "", err
}
}

// InferImageFormat recognizes all known image formats (including non-bootable
// imageformat.Infer recognizes all known image formats (including non-bootable
// ones like OCI tarballs); reject formats that QEMU can't boot here.
if !lo.Contains(BootableImageFormats(), imageFormat) {
return "", "", fmt.Errorf(
Expand Down Expand Up @@ -598,7 +589,7 @@ func fileExtensionsForFormat(format string) []string {
switch format {
case string(ImageFormatVhd):
return []string{"vhd", "vhdfixed"}
case string(ImageFormatOCI):
case imageformat.OCI:
return []string{"oci.tar.xz", "oci.tar.gz", "oci.tar"}
default:
return []string{format}
Expand Down Expand Up @@ -682,43 +673,6 @@ func findImageArtifact(
)
}

// 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(
"cannot infer image format from %#q: no file extension",
imagePath,
)
}

// Strip the leading dot (e.g., ".vhdfixed" → "vhdfixed").
format := ext[1:]
if format == "vhdfixed" {
format = string(ImageFormatVhd)
}

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

return format, nil
}

// listImageArtifacts returns a list of image artifact filenames in the given directory.
func listImageArtifacts(env *azldev.Env, dir string) ([]string, error) {
entries, err := fileutils.ReadDir(env.FS(), dir)
Expand Down
46 changes: 0 additions & 46 deletions internal/app/azldev/cmds/image/boot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,52 +181,6 @@ func TestBootableImageFormats(t *testing.T) {
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) {
tests := []struct {
name string
path string
expected string
}{
{name: "raw", path: "/path/to/image.raw", expected: "raw"},
{name: "qcow2", path: "/path/to/image.qcow2", expected: "qcow2"},
{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 {
t.Run(test.name, func(t *testing.T) {
format, err := image.InferImageFormat(test.path)
require.NoError(t, err)
assert.Equal(t, test.expected, format)
})
}
}

func TestInferImageFormat_NoExtension(t *testing.T) {
_, err := image.InferImageFormat("/path/to/imagefile")
require.Error(t, err)
assert.Contains(t, err.Error(), "no file extension")
}

func TestInferImageFormat_UnsupportedExtension(t *testing.T) {
_, err := image.InferImageFormat("/path/to/image.iso")
require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported image format")
assert.Contains(t, err.Error(), "iso")
}

func TestSupportedArchitectures(t *testing.T) {
archs := qemu.SupportedArchitectures()
require.NotEmpty(t, archs)
Expand Down
11 changes: 9 additions & 2 deletions internal/app/azldev/cmds/image/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import (

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/imagebuilder"
"github.com/microsoft/azure-linux-dev-tools/internal/lisa"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/pytest"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/imageformat"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -182,7 +184,7 @@ func resolveImageTestPath(env *azldev.Env, options *ImageTestOptions) (string, e
return options.ImagePath, nil
}

imagePath, _, err := findImageArtifact(env, options.ImageName, "", AllImageFormats())
imagePath, _, err := findImageArtifact(env, options.ImageName, "", imageformat.All())
if err != nil {
return "", err
}
Expand Down Expand Up @@ -260,7 +262,12 @@ func runResolvedTest(
})

case string(projectconfig.TestTypeLisa):
return RunLisaTestDefinition(env, resolvedTest, imageConfig, options)
//nolint:wrapcheck // Preserve the existing command-facing diagnostic unchanged.
return lisa.RunTestDefinition(env, resolvedTest, imageConfig, &lisa.Options{
ImageName: options.ImageName,
ImagePath: options.ImagePath,
LisaDir: options.LisaDir,
})

case "tmt":
return fmt.Errorf(
Expand Down
32 changes: 0 additions & 32 deletions internal/app/azldev/cmds/image/test_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/testutils"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -65,34 +64,3 @@ func TestResolveImageTestsToRun_NoWarnWhenOnlyNewTestsPresent(t *testing.T) {
require.NoError(t, err)
assert.NotContains(t, buf.String(), "ignored")
}

func TestResolveLisaFrameworkDir_UsesLisaDirWhenSet(t *testing.T) {
testEnv := testutils.NewTestEnv(t)
require.NoError(t, fileutils.MkdirAll(testEnv.Env.FS(), "/checkouts/lisa"))

options := &ImageTestOptions{LisaDir: "/checkouts/lisa"}

dir, err := resolveLisaFrameworkDir(testEnv.Env, "my-test", "/work/lisa", nil, options)
require.NoError(t, err)
assert.Equal(t, "/checkouts/lisa", dir)
}

func TestResolveLisaFrameworkDir_LisaDirMissingReturnsError(t *testing.T) {
testEnv := testutils.NewTestEnv(t)

options := &ImageTestOptions{LisaDir: "/does/not/exist"}

_, err := resolveLisaFrameworkDir(testEnv.Env, "my-test", "/work/lisa", nil, options)
require.Error(t, err)
assert.Contains(t, err.Error(), "does not exist")
}

func TestResolveLisaFrameworkDir_NoLisaDirAndNoFrameworkReturnsError(t *testing.T) {
testEnv := testutils.NewTestEnv(t)

options := &ImageTestOptions{}

_, err := resolveLisaFrameworkDir(testEnv.Env, "my-test", "/work/lisa", nil, options)
require.Error(t, err)
assert.Contains(t, err.Error(), "--lisa-dir")
}
62 changes: 62 additions & 0 deletions internal/app/azldev/cmds/image/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,65 @@ func TestImageTestPreservesPytestOptions(t *testing.T) {
"--path", "/images/candidate.raw", "--junit-xml", "/reports/results.xml",
}}, commands)
}

func TestImageTestPreservesLISAOptions(t *testing.T) {
testEnv := testutils.NewTestEnv(t)
testEnv.Config.Tests["smoke"] = projectconfig.TestDefinition{
Type: string(projectconfig.TestTypeLisa),
Lisa: map[string]any{
"name": "selected-case",
"extra-args": []any{
"--image", "{image-name}", "--path", "{image-path}",
},
},
}
testEnv.Config.Images["selected-image"] = projectconfig.ImageConfig{
Name: "configured-image",
Tests: &projectconfig.ImageTestsConfig{
Tests: []projectconfig.TestRef{{Name: "smoke"}},
},
}

frameworkDir := "/checkouts/lisa"

venvDir := filepath.Join(testEnv.Env.WorkDir(), "lisa", "venv", "smoke")
for _, directory := range []string{frameworkDir, "/images", filepath.Join(venvDir, "bin")} {
require.NoError(t, fileutils.MkdirAll(testEnv.TestFS, directory))
}

require.NoError(t, fileutils.WriteFile(
testEnv.TestFS, filepath.Join(venvDir, "bin", "python3"), nil, fileperms.PublicExecutable,
))
require.NoError(t, fileutils.WriteFile(testEnv.TestFS, "/images/candidate.qcow2", nil, fileperms.PublicFile))
testEnv.CmdFactory.RegisterCommandInSearchPath("python3")
testEnv.CmdFactory.RegisterCommandInSearchPath("ssh-keygen")

runbookPath := filepath.Join(frameworkDir, "azldev-generated-smoke.yml")

var lisaCommands [][]string

testEnv.CmdFactory.RunHandler = func(cmd *exec.Cmd) error {
if filepath.Base(cmd.Args[0]) == "lisa" {
require.Equal(t, filepath.Join(frameworkDir, "lisa"), cmd.Dir)
lisaCommands = append(lisaCommands, cmd.Args)
runbook, err := fileutils.ReadFile(testEnv.TestFS, runbookPath)
require.NoError(t, err)
assert.Contains(t, string(runbook), "selected-case")
assert.Contains(t, string(runbook), "/images/candidate.qcow2")
}

return nil
}
cmd := image.NewImageTestCmd()
cmd.SetArgs([]string{
"selected-image", "--image-path", "/images/candidate.qcow2", "--lisa-dir", frameworkDir,
})
require.NoError(t, cmd.ExecuteContext(testEnv.Env))
require.Equal(t, [][]string{{
filepath.Join(venvDir, "bin", "lisa"), "-r", runbookPath,
"--image", "selected-image", "--path", "/images/candidate.qcow2",
}}, lisaCommands)
exists, err := fileutils.Exists(testEnv.TestFS, runbookPath)
require.NoError(t, err)
assert.False(t, exists, "generated runbook should still be removed")
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package image
package lisa

import (
"errors"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package image
package lisa

import (
"testing"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package image
package lisa

import (
"fmt"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package image
package lisa

import (
"testing"
Expand Down
Loading
Loading