diff --git a/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go b/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go index eac2c84970..15b85b35a8 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go +++ b/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go @@ -48,7 +48,6 @@ var ( ErrAdditionalDirsSourceIsFile = NewImageCustomizerError("Validation:AdditionalDirsSourceIsFile", "additionalDirs source exists but is a file") ErrAdditionalDirsSourceNotFound = NewImageCustomizerError("Validation:AdditionalDirsSourceNotFound", "additionalDirs source does not exist") ErrInvalidPackageSnapshotTime = NewImageCustomizerError("Validation:InvalidPackageSnapshotTime", "invalid command-line option '--package-snapshot-time'") - ErrUnsupportedFedoraFeature = NewImageCustomizerError("Validation:UnsupportedFedoraFeature", "unsupported feature for Fedora images") ErrInvalidOutputSelinuxPolicyPathArg = NewImageCustomizerError("Validation:InvalidOutputSelinuxPolicyPathArg", "invalid command-line option '--output-selinux-policy-path'") ErrOutputSelinuxPolicyPathIsFileArg = NewImageCustomizerError("Validation:OutputSelinuxPolicyPathIsFileArg", "path exists but is a file") ErrInvalidOutputSelinuxPolicyPathConfig = NewImageCustomizerError("Validation:InvalidOutputSelinuxPolicyPathConfig", "invalid config file property 'output.selinuxPolicyPath'") diff --git a/toolkit/tools/pkg/imagecustomizerlib/convertimage_test.go b/toolkit/tools/pkg/imagecustomizerlib/convertimage_test.go index c832a3091f..4e935b6298 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/convertimage_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/convertimage_test.go @@ -123,7 +123,7 @@ func testConvertImageRawToCosi(t *testing.T, baseImageInfo testBaseImageInfo) { if baseImageInfo.Distro == baseImageDistroUbuntu { // This check should be removed once bootloader hard-reset support is added for Ubuntu. // It will fail once this support is added. - assert.ErrorContains(t, err, "bootloader hard-reset is not supported for Ubuntu images") + assert.ErrorIs(t, err, ErrUbuntuUnsupportedBootloaderHardReset) return } if !assert.NoError(t, err) { @@ -197,7 +197,7 @@ func testConvertImageRawToCosiWithCompression(t *testing.T, baseImageInfo testBa if baseImageInfo.Distro == baseImageDistroUbuntu { // This check should be removed once bootloader hard-reset support is added for Ubuntu. // It will fail once this support is added. - assert.ErrorContains(t, err, "bootloader hard-reset is not supported for Ubuntu images") + assert.ErrorIs(t, err, ErrUbuntuUnsupportedBootloaderHardReset) return } if !assert.NoError(t, err) { diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go index 85be4cb021..ab304bcd07 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler.go @@ -27,6 +27,10 @@ const ( var ( ErrUnsupportedDistroVersion = NewImageCustomizerError("Validation:UnsupportedDistroVersion", "base image has unsupported distro version") ErrUnsupportedDistroVersionSuffix = fmt.Sprintf("preview feature '%s' may be specified to use unsupported versions", imagecustomizerapi.PreviewFeatureUnsupportedDistroVersion) + + ErrUnsupportedDistroApi = NewImageCustomizerError("Validation:UnsupportedDistroApi", "unsupported API for distro") + ErrUnsupportedPackageSnapshotTime = NewImageCustomizerError("Validation:UnsupportedPackageSnapshotTime", "package snapshot time API is not supported") + ErrUnsupportedRpmSources = NewImageCustomizerError("Validation:UnsupportedRpmSources", "RPM sources API is not supported") ) // DistroHandler represents the interface for distribution-specific configuration diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go index dea83e0834..818f1f9d3c 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_acl.go @@ -58,6 +58,16 @@ func (d *aclDistroHandler) ValidateConfig(rc *ResolvedConfig) error { } } + err := d.checkForUnsupportedApis(rc) + if err != nil { + return fmt.Errorf("%w (distro='%s', versionid='%s'):\n%w", ErrUnsupportedDistroApi, d.targetOs.Distro, + d.targetOs.VersionId, err) + } + + return nil +} + +func (d *aclDistroHandler) checkForUnsupportedApis(rc *ResolvedConfig) error { if rc.Storage.CustomizePartitions() { return fmt.Errorf("storage repartitioning is not yet supported for ACL") } diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go index f6e371acd8..9c0fe32eb3 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_azurelinux4.go @@ -60,10 +60,24 @@ func (d *azureLinux4DistroHandler) ValidateConfig(rc *ResolvedConfig) error { } } + err := d.checkForUnsupportedApis(rc) + if err != nil { + return fmt.Errorf("%w (distro='%s', versionid='%s'):\n%w", ErrUnsupportedDistroApi, d.targetOs.Distro, + d.targetOs.VersionId, err) + } + + return nil +} + +func (d *azureLinux4DistroHandler) checkForUnsupportedApis(rc *ResolvedConfig) error { + if rc.HasPackageSnapshotTime() { + return ErrUnsupportedPackageSnapshotTime + } + switch rc.OutputImageFormat { case imagecustomizerapi.ImageFormatTypeIso, imagecustomizerapi.ImageFormatTypePxeDir, imagecustomizerapi.ImageFormatTypePxeTar: - return fmt.Errorf("ISO and PXE output formats are not supported for Azure Linux 4.0") + return fmt.Errorf("ISO and PXE output formats are not supported yet for Azure Linux 4.0 images") } return nil diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go index eac2bde3ea..e7819f463f 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_fedora.go @@ -66,8 +66,18 @@ func (d *fedoraDistroHandler) ValidateConfig(rc *ResolvedConfig) error { } } + err := d.checkForUnsupportedApis(rc) + if err != nil { + return fmt.Errorf("%w (distro='%s', versionid='%s'):\n%w", ErrUnsupportedDistroApi, d.targetOs.Distro, + d.targetOs.VersionId, err) + } + + return nil +} + +func (d *fedoraDistroHandler) checkForUnsupportedApis(rc *ResolvedConfig) error { if rc.HasPackageSnapshotTime() { - return fmt.Errorf("Package snapshotting API not supported for Fedora:\n%w", ErrUnsupportedFedoraFeature) + return ErrUnsupportedPackageSnapshotTime } return nil diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_test.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_test.go index 6910d0c588..fdaa078c16 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_test.go @@ -6,6 +6,7 @@ package imagecustomizerlib import ( "os" "path/filepath" + "slices" "testing" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" @@ -143,3 +144,94 @@ func TestAclValidateConfigPackageOpsRequireToolsDir(t *testing.T) { err = handler.ValidateConfig(rc) assert.NoError(t, err) } + +func TestCustomizeImageUnsupportedPackageSnapshotTime(t *testing.T) { + for _, baseImageInfo := range slices.Concat([]testBaseImageInfo{testBaseImageAzl4CoreEfi}, baseImageUbuntuAll) { + t.Run(baseImageInfo.Name, func(t *testing.T) { + testCustomizeImageUnsupportedPackageSnapshotTimeHelper(t, baseImageInfo) + }) + } +} + +func testCustomizeImageUnsupportedPackageSnapshotTimeHelper(t *testing.T, baseImageInfo testBaseImageInfo) { + baseImage := checkSkipForCustomizeImage(t, baseImageInfo) + + testTmpDir := filepath.Join(tmpDir, "TestCustomizeImageUnsupportedPackageSnapshotTime_"+baseImageInfo.Name) + defer os.RemoveAll(testTmpDir) + + buildDir := filepath.Join(testTmpDir, "build") + + options := ImageCustomizerOptions{ + BuildDir: buildDir, + InputImageFile: baseImage, + OutputImageFile: "./out/image.vhdx", + OutputImageFormat: "vhdx", + UseBaseImageRpmRepos: true, + PreviewFeatures: baseImageInfo.PreviewFeatures, + } + + config := &imagecustomizerapi.Config{ + PreviewFeatures: []imagecustomizerapi.PreviewFeature{imagecustomizerapi.PreviewFeaturePackageSnapshotTime}, + OS: &imagecustomizerapi.OS{}, + } + + options.PackageSnapshotTime = "2025-01-01" + + err := CustomizeImage(t.Context(), testTmpDir, config, options) + assert.ErrorIs(t, err, ErrUnsupportedPackageSnapshotTime) + assert.ErrorIs(t, err, ErrUnsupportedDistroApi) + + options.PackageSnapshotTime = "" + config.OS.Packages.SnapshotTime = "2025-01-01" + + err = CustomizeImage(t.Context(), testTmpDir, config, options) + assert.ErrorIs(t, err, ErrUnsupportedPackageSnapshotTime) + assert.ErrorIs(t, err, ErrUnsupportedDistroApi) +} + +func TestCustomizeImageUnsupportedRpmSources(t *testing.T) { + for _, baseImageInfo := range baseImageUbuntuAll { + t.Run(baseImageInfo.Name, func(t *testing.T) { + testCustomizeImageUnsupportedRpmSourcesHelper(t, baseImageInfo) + }) + } +} + +func testCustomizeImageUnsupportedRpmSourcesHelper(t *testing.T, baseImageInfo testBaseImageInfo) { + baseImage := checkSkipForCustomizeImage(t, baseImageInfo) + + testTmpDir := filepath.Join(tmpDir, "TestCustomizeImageUnsupportedRpmSources_"+baseImageInfo.Name) + defer os.RemoveAll(testTmpDir) + + buildDir := filepath.Join(testTmpDir, "build") + + err := os.MkdirAll(testTmpDir, os.ModePerm) + if !assert.NoError(t, err) { + return + } + + repoFile := filepath.Join(testTmpDir, "a.repo") + err = os.WriteFile(repoFile, []byte{}, os.ModePerm) + if !assert.NoError(t, err) { + return + } + + options := ImageCustomizerOptions{ + BuildDir: buildDir, + InputImageFile: baseImage, + OutputImageFile: "./out/image.vhdx", + OutputImageFormat: "vhdx", + UseBaseImageRpmRepos: true, + PreviewFeatures: baseImageInfo.PreviewFeatures, + } + + config := &imagecustomizerapi.Config{ + OS: &imagecustomizerapi.OS{}, + } + + options.RpmsSources = []string{repoFile} + + err = CustomizeImage(t.Context(), testTmpDir, config, options) + assert.ErrorIs(t, err, ErrUnsupportedRpmSources) + assert.ErrorIs(t, err, ErrUnsupportedDistroApi) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go index dbc9610595..70d72155d0 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu.go @@ -31,6 +31,11 @@ const ( grubEfiPackageDebianArm64 = "grub-efi-arm64" ) +var ( + ErrUbuntuUnsupportedBootloaderHardReset = NewImageCustomizerError("Validation:UbuntuUnsupportedBootloaderHardReset", "bootloader hard-reset API is not supported yet for Ubuntu images") + ErrUbuntuUnsupportedDisableBaseImageRepos = NewImageCustomizerError("Validation:UbuntuUnsupportedDisableBaseImageRepos", "disabling base image package repositories is not supported yet for Ubuntu images") +) + func newUbuntuDistroHandler(targetOs targetos.TargetOs) *ubuntuDistroHandler { logger.Log.Debugf("Distro handler: Ubuntu (distro='%s', versionid='%s')", targetOs.Distro, targetOs.VersionId) @@ -59,37 +64,45 @@ func (d *ubuntuDistroHandler) ValidateConfig(rc *ResolvedConfig) error { } } - // Check if Ubuntu is being used with bootloader hard-reset. - // Ubuntu bootloader config logic is not yet fully implemented. - if rc.BootLoader.ResetType == imagecustomizerapi.ResetBootLoaderTypeHard { - return ErrUbuntuBootLoaderHardReset + err := d.checkForUnsupportedApis(rc) + if err != nil { + return fmt.Errorf("%w (distro='%s', versionid='%s'):\n%w", ErrUnsupportedDistroApi, d.targetOs.Distro, + d.targetOs.VersionId, err) } return nil } -// ManagePackages handles the complete package management workflow for Ubuntu -func (d *ubuntuDistroHandler) ManagePackages(ctx context.Context, buildDir string, baseConfigPath string, - config *imagecustomizerapi.OS, imageChroot *safechroot.Chroot, toolsChroot *safechroot.Chroot, - rpmsSources []string, useBaseImageRpmRepos bool, snapshotTime imagecustomizerapi.PackageSnapshotTime, -) error { - if len(rpmsSources) > 0 { - return fmt.Errorf("RPM sources are not supported for Ubuntu images:\n%w", ErrUnsupportedUbuntuFeature) +func (d *ubuntuDistroHandler) checkForUnsupportedApis(rc *ResolvedConfig) error { + // Check if Ubuntu is being used with bootloader hard-reset. + // Ubuntu bootloader config logic is not yet fully implemented. + if rc.BootLoader.ResetType == imagecustomizerapi.ResetBootLoaderTypeHard { + return ErrUbuntuUnsupportedBootloaderHardReset + } + + if len(rc.Options.RpmsSources) > 0 { + return ErrUnsupportedRpmSources } // UseBaseImageRpmRepos defaults to true and is only false when the user explicitly // passes --disable-base-image-rpm-repos. Ubuntu does not use RPM repos, so disabling // them is not meaningful and likely indicates a configuration mistake. - if !useBaseImageRpmRepos { - return fmt.Errorf("Disabling base image RPM repositories is not supported for Ubuntu images:\n%w", - ErrUnsupportedUbuntuFeature) + if !rc.Options.UseBaseImageRpmRepos { + return ErrUbuntuUnsupportedDisableBaseImageRepos } - if config.Packages.SnapshotTime != "" { - return fmt.Errorf("package snapshotTime is not yet supported for Ubuntu images:\n%w", - ErrUnsupportedUbuntuFeature) + if rc.HasPackageSnapshotTime() { + return ErrUnsupportedPackageSnapshotTime } + return nil +} + +// ManagePackages handles the complete package management workflow for Ubuntu +func (d *ubuntuDistroHandler) ManagePackages(ctx context.Context, buildDir string, baseConfigPath string, + config *imagecustomizerapi.OS, imageChroot *safechroot.Chroot, toolsChroot *safechroot.Chroot, + rpmsSources []string, useBaseImageRpmRepos bool, snapshotTime imagecustomizerapi.PackageSnapshotTime, +) error { return managePackagesDeb(ctx, config, imageChroot) } @@ -100,8 +113,7 @@ func (d *ubuntuDistroHandler) IsPackageInstalled(imageChroot safechroot.ChrootIn func (d *ubuntuDistroHandler) GetPackageInformation(imageChroot *safechroot.Chroot, packageName string, ) (*PackageVersionInformation, error) { - return nil, fmt.Errorf("Getting package information is not supported yet for Ubuntu images:\n%w", - ErrUnsupportedUbuntuFeature) + return nil, fmt.Errorf("getting package information is not supported yet for Ubuntu images") } func (d *ubuntuDistroHandler) GetAllPackagesFromChroot(imageChroot safechroot.ChrootInterface) ([]OsPackage, error) { @@ -194,7 +206,7 @@ func (d *ubuntuDistroHandler) ConfigureDiskBootLoader(imageConnection *imageconn selinuxConfig imagecustomizerapi.SELinux, kernelCommandLine imagecustomizerapi.KernelCommandLine, currentSELinuxMode imagecustomizerapi.SELinuxMode, newImage bool, ) error { - return ErrUbuntuBootLoaderHardReset + return ErrUbuntuUnsupportedBootloaderHardReset } func (d *ubuntuDistroHandler) ReadGrubConfigLinuxArgs(bootDir string) (map[string][]grubConfigLinuxArg, error) { diff --git a/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu_test.go b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu_test.go new file mode 100644 index 0000000000..22f27ff08d --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/distrohandler_ubuntu_test.go @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package imagecustomizerlib + +import ( + "os" + "path/filepath" + "testing" + + "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" + "github.com/stretchr/testify/assert" +) + +func TestCustomizeImageUbuntuUnsupportedAPIs(t *testing.T) { + for _, baseImageInfo := range baseImageUbuntuAll { + t.Run(baseImageInfo.Name, func(t *testing.T) { + testCustomizeImageUbuntuUnsupportedAPIsHelper(t, baseImageInfo) + }) + } +} + +func testCustomizeImageUbuntuUnsupportedAPIsHelper(t *testing.T, baseImageInfo testBaseImageInfo) { + baseImage := checkSkipForCustomizeImage(t, baseImageInfo) + + testTmpDir := filepath.Join(tmpDir, "TestCustomizeImageUbuntuUnsupportedAPIs_"+baseImageInfo.Name) + defer os.RemoveAll(testTmpDir) + + buildDir := filepath.Join(testTmpDir, "build") + + options := ImageCustomizerOptions{ + BuildDir: buildDir, + InputImageFile: baseImage, + OutputImageFile: "./out/image.vhdx", + OutputImageFormat: "vhdx", + UseBaseImageRpmRepos: true, + PreviewFeatures: baseImageInfo.PreviewFeatures, + } + + config := &imagecustomizerapi.Config{ + OS: &imagecustomizerapi.OS{}, + } + + config.OS.BootLoader.ResetType = imagecustomizerapi.ResetBootLoaderTypeHard + + err := CustomizeImage(t.Context(), testTmpDir, config, options) + assert.ErrorIs(t, err, ErrUbuntuUnsupportedBootloaderHardReset) + assert.ErrorIs(t, err, ErrUnsupportedDistroApi) + + config.OS.BootLoader.ResetType = imagecustomizerapi.ResetBootLoaderTypeDefault + options.UseBaseImageRpmRepos = false + + err = CustomizeImage(t.Context(), testTmpDir, config, options) + assert.ErrorIs(t, err, ErrUbuntuUnsupportedDisableBaseImageRepos) + assert.ErrorIs(t, err, ErrUnsupportedDistroApi) +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index b2a5ab2a54..6552ddf023 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -42,8 +42,6 @@ var ( ErrFedoraPreviewFeatureRequired = NewImageCustomizerError("Validation:FedoraPreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Fedora base image", imagecustomizerapi.PreviewFeatureFedora)) ErrUbuntuPreviewFeatureRequired = NewImageCustomizerError("Validation:UbuntuPreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Ubuntu base image", imagecustomizerapi.PreviewFeatureUbuntu)) ErrAzureContainerLinuxPreviewFeatureRequired = NewImageCustomizerError("Validation:AzureContainerLinuxPreviewFeatureRequired", fmt.Sprintf("preview feature '%s' required to customize Azure Container Linux base image", imagecustomizerapi.PreviewFeatureAzureContainerLinux)) - ErrUbuntuBootLoaderHardReset = NewImageCustomizerError("Validation:UbuntuBootLoaderHardReset", "bootloader hard-reset is not supported for Ubuntu images") - ErrUnsupportedUbuntuFeature = NewImageCustomizerError("Validation:UnsupportedUbuntuFeature", "unsupported feature for Ubuntu images") ErrInputImageOciPreviewRequired = NewImageCustomizerError("Validation:InputImageOciPreviewRequired", fmt.Sprintf("preview feature '%s' required to specify OCI input image", imagecustomizerapi.PreviewFeatureInputImageOci)) ErrConvertUnsupportedInputFormat = NewImageCustomizerError("Validation:ConvertUnsupportedInputFormat", "input image format is not supported") ErrConvertBuildDirRequired = NewImageCustomizerError("Validation:ConvertBuildDirRequired", "build directory is required for cosi and baremetal-image output formats") diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder_test.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder_test.go index ad67a7b330..b4d827e745 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder_test.go @@ -29,7 +29,7 @@ import ( // Azure Linux distro handler when ISO or PXE output is requested with // an Azure Linux 4.0 base image. ISO and PXE outputs are not supported // on Azure Linux 4.0; LiveOS tests expect this error in that case. -const azl4IsoPxeUnsupportedError = "ISO and PXE output formats are not supported for Azure Linux 4.0" +const azl4IsoPxeUnsupportedError = "ISO and PXE output formats are not supported yet for Azure Linux 4.0 images" func createConfig(t *testing.T, baseImageVersion string, fileNames, kernelParameter string, initramfsType imagecustomizerapi.InitramfsImageType, bootstrapFileUrl string, enlargeDisk, enableOsConfig, bootstrapPrereqs, twoKernels bool, kdumpBootFiles imagecustomizerapi.KdumpBootFilesType,