diff --git a/docs/imagecustomizer/api/configuration/baseConfig.md b/docs/imagecustomizer/api/configuration/baseConfig.md index 1fd46b7d6c..fe36bcf49e 100644 --- a/docs/imagecustomizer/api/configuration/baseConfig.md +++ b/docs/imagecustomizer/api/configuration/baseConfig.md @@ -51,6 +51,29 @@ or are processed sequentially. - `"hard-reset"` - `""` (i.e. no reset) +**Storage Override Fields:** + +Current value (if specified) fully overrides base's value +- `.storage.disks` +- `.storage.verity` +- `.storage.bootType` +- `.storage.filesystems` + +**Storage configuration also follows specific rules based on field combinations:** + +| Base\Current | None | `.storage.disks` | `.storage.resetPartitionsUuidsType` | `.storage.reinitializeVerity` | +|--------------|------|------------------|-------------------------------------|------------------------------| +| **None** | None | Current's `.storage.disks` | Current's `.storage.resetPartitionsUuidsType` | Current's `.storage.reinitializeVerity` | +| **`.storage.disks`** | Base's `.storage.disks` | Current's `.storage.disks` | Error¹ | Error² | +| **`.resetPartitionsUuidsType`** | Base's `.storage.resetPartitionsUuidsType` | Current's `.storage.disks` | Current's `.storage.resetPartitionsUuidsType` | Base's reset + Current's reinit³ | +| **`.storage.reinitializeVerity`** | Base's `.storage.reinitializeVerity` | Current's `.storage.disks` | Base's reinit + Current's reset³ | Current's `.storage.reinitializeVerity` | + +¹ The current config requesting the partition UUIDs to be reset when the base config customized the partition table is somewhat pointless. Hence we error out. + +² Cannot reinitialize verity when base config destroys original verity setup with partition layout changes. + +³ Base's `.resetPartitionsUuidsType` and current's `.reinitializeVerity` are compatible and both are preserved together since UUID reset updates partition identifiers while verity reinit updates verity mapping to use new identifiers and rebuilds hash trees. + ## path [string] Required. diff --git a/toolkit/tools/imagecustomizerapi/config.go b/toolkit/tools/imagecustomizerapi/config.go index b7efe4ee51..a68b112e1b 100644 --- a/toolkit/tools/imagecustomizerapi/config.go +++ b/toolkit/tools/imagecustomizerapi/config.go @@ -88,7 +88,7 @@ func (c *Config) IsValid() (err error) { return err } - if c.CustomizePartitions() && !hasResetBootLoader { + if c.Storage.CustomizePartitions() && !hasResetBootLoader { return fmt.Errorf("'os.bootloader.reset' must be specified if 'storage.disks' is specified") } @@ -150,7 +150,3 @@ func (c *Config) IsValid() (err error) { return nil } - -func (c *Config) CustomizePartitions() bool { - return c.Storage.CustomizePartitions() -} diff --git a/toolkit/tools/pkg/imagecreatorlib/imagecreator.go b/toolkit/tools/pkg/imagecreatorlib/imagecreator.go index b07343464a..b7127d9337 100644 --- a/toolkit/tools/pkg/imagecreatorlib/imagecreator.go +++ b/toolkit/tools/pkg/imagecreatorlib/imagecreator.go @@ -26,15 +26,13 @@ func CreateImageWithConfigFile(ctx context.Context, buildDir string, configFile return fmt.Errorf("failed to unmarshal config file %s:\n%w", configFile, err) } - baseConfigPath, _ := filepath.Split(configFile) - - absBaseConfigPath, err := filepath.Abs(baseConfigPath) + absConfigFile, err := filepath.Abs(configFile) if err != nil { - return fmt.Errorf("failed to get absolute path of config file directory:\n%w", err) + return fmt.Errorf("failed to get absolute path of config file:\n%w", err) } err = createNewImage( - ctx, buildDir, absBaseConfigPath, config, rpmsSources, outputImageFile, + ctx, buildDir, absConfigFile, config, rpmsSources, outputImageFile, outputImageFormat, toolsTar, distro, distroVersion, packageSnapshotTime) if err != nil { return err @@ -43,12 +41,12 @@ func CreateImageWithConfigFile(ctx context.Context, buildDir string, configFile return nil } -func createNewImage(ctx context.Context, buildDir string, baseConfigPath string, config imagecustomizerapi.Config, +func createNewImage(ctx context.Context, buildDir string, configFile string, config imagecustomizerapi.Config, rpmsSources []string, outputImageFile string, outputImageFormat string, toolsTar string, distro string, distroVersion string, packageSnapshotTime string, ) error { rc, err := validateConfig( - ctx, baseConfigPath, &config, rpmsSources, toolsTar, outputImageFile, + ctx, configFile, &config, rpmsSources, toolsTar, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) if err != nil { return err @@ -73,7 +71,7 @@ func createNewImage(ctx context.Context, buildDir string, baseConfigPath string, return err } - disks := rc.Config.Storage.Disks + disks := rc.Storage.Disks diskConfig := disks[0] installOSFunc := func(imageChroot *safechroot.Chroot) error { return nil @@ -86,7 +84,7 @@ func createNewImage(ctx context.Context, buildDir string, baseConfigPath string, partIdToPartUuid, err := imagecustomizerlib.CreateNewImage( distroHandler.GetTargetOs(), rc.RawImageFile, - diskConfig, rc.Config.Storage.FileSystems, + diskConfig, rc.Storage.FileSystems, rc.BuildDirAbs, setupRoot, installOSFunc) if err != nil { return err diff --git a/toolkit/tools/pkg/imagecreatorlib/validation.go b/toolkit/tools/pkg/imagecreatorlib/validation.go index ec7e4c07df..a43c7470b9 100644 --- a/toolkit/tools/pkg/imagecreatorlib/validation.go +++ b/toolkit/tools/pkg/imagecreatorlib/validation.go @@ -66,22 +66,22 @@ func validateSupportedOsFields(osConfig *imagecustomizerapi.OS) error { return nil } -func validateConfig(ctx context.Context, baseConfigPath string, config *imagecustomizerapi.Config, rpmsSources []string, +func validateConfig(ctx context.Context, configFile string, config *imagecustomizerapi.Config, rpmsSources []string, toolsTar string, outputImageFile, outputImageFormat string, packageSnapshotTime string, buildDir string, ) (*imagecustomizerlib.ResolvedConfig, error) { err := validateSupportedFields(config) if err != nil { - return nil, fmt.Errorf("invalid config file (%s):\n%w", baseConfigPath, err) + return nil, fmt.Errorf("invalid config file (%s):\n%w", configFile, err) } // Validate mandatory fields for creating a seed image - err = validateMandatoryFields(baseConfigPath, config, rpmsSources, toolsTar) + err = validateMandatoryFields(configFile, config, rpmsSources, toolsTar) if err != nil { return nil, err } // TODO: Validate for distro and release - rc, err := imagecustomizerlib.ValidateConfig(ctx, baseConfigPath, config, true, + rc, err := imagecustomizerlib.ValidateConfig(ctx, configFile, config, true, imagecustomizerlib.ImageCustomizerOptions{ RpmsSources: rpmsSources, OutputImageFile: outputImageFile, @@ -100,10 +100,10 @@ func validateConfig(ctx context.Context, baseConfigPath string, config *imagecus return rc, nil } -func validateMandatoryFields(baseConfigPath string, config *imagecustomizerapi.Config, rpmsSources []string, toolsTar string) error { +func validateMandatoryFields(configFile string, config *imagecustomizerapi.Config, rpmsSources []string, toolsTar string) error { // check if storage disks is not empty for creating a seed image if len(config.Storage.Disks) == 0 { - return fmt.Errorf("storage.disks field is required in the config file (%s)", baseConfigPath) + return fmt.Errorf("storage.disks field is required in the config file (%s)", configFile) } // rpmSources must not be empty for creating a seed image diff --git a/toolkit/tools/pkg/imagecreatorlib/validation_test.go b/toolkit/tools/pkg/imagecreatorlib/validation_test.go index 894b0d99c8..75b8712e2e 100644 --- a/toolkit/tools/pkg/imagecreatorlib/validation_test.go +++ b/toolkit/tools/pkg/imagecreatorlib/validation_test.go @@ -97,21 +97,21 @@ func TestValidateOutput_AcceptsValidPaths(t *testing.T) { packageSnapshotTime := "" // The output image file can be sepcified as an argument without being in specified the config. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) outputImageFile = outputImageFileNewRelativeCwd // The output image file can be specified as an argument relative to the current working directory. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) outputImageFile = outputImageDir // The output image file, specified as an argument, must not be a directory. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.Error(t, err) assert.ErrorContains(t, err, "is a directory") @@ -119,7 +119,7 @@ func TestValidateOutput_AcceptsValidPaths(t *testing.T) { outputImageFile = outputImageDirRelativeCwd // The above is also true for relative paths. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.Error(t, err) assert.ErrorContains(t, err, "is a directory") @@ -127,14 +127,14 @@ func TestValidateOutput_AcceptsValidPaths(t *testing.T) { outputImageFile = outputImageFileExists // The output image file, specified as an argument, may be a file that already exists. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) outputImageFile = outputImageFileExistsRelativeCwd // The above is also true for relative paths. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) @@ -142,21 +142,21 @@ func TestValidateOutput_AcceptsValidPaths(t *testing.T) { config.Output.Image.Path = outputImageFileNew // The output image file cab be specified in the config without being specified as an argument. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) config.Output.Image.Path = outputImageFileNewRelativeConfig // The output image file can be specified in the config relative to the base config path. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) config.Output.Image.Path = outputImageDir // The output image file, specified in the config, must not be a directory. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.Error(t, err) assert.ErrorContains(t, err, "is a directory") @@ -164,7 +164,7 @@ func TestValidateOutput_AcceptsValidPaths(t *testing.T) { config.Output.Image.Path = outputImageDirRelativeConfig // The above is also true for relative paths. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.Error(t, err) assert.ErrorContains(t, err, "is a directory") @@ -172,14 +172,14 @@ func TestValidateOutput_AcceptsValidPaths(t *testing.T) { config.Output.Image.Path = outputImageFileExists // The output image file, specified in the config, may be a file that already exists. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) config.Output.Image.Path = outputImageFileExistsRelativeConfig // The above is also true for relative paths. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) @@ -187,20 +187,20 @@ func TestValidateOutput_AcceptsValidPaths(t *testing.T) { config.Output.Image.Path = outputImageFileNew // The output image file can be specified both as an argument and in the config. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) config.Output.Image.Path = outputImageDir // The output image file can even be invalid in the config if it is specified as an argument. - _, err = validateConfig(t.Context(), baseConfigPath, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.NoError(t, err) } func TestValidateConfig_EmptyConfig(t *testing.T) { - baseConfigPath := testDir + configFile := filepath.Join(testDir, "empty-config.yaml") config := &imagecustomizerapi.Config{} rpmSources := []string{} @@ -209,7 +209,7 @@ func TestValidateConfig_EmptyConfig(t *testing.T) { packageSnapshotTime := "" buildDir := "./" - _, err := validateConfig(t.Context(), baseConfigPath, config, rpmSources, "", outputImageFile, outputImageFormat, + _, err := validateConfig(t.Context(), configFile, config, rpmSources, "", outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.ErrorContains(t, err, "storage.disks field is required in the config file") } @@ -236,6 +236,7 @@ func TestValidateConfig_EmptyPackagestoInstall(t *testing.T) { assert.NoError(t, err) // Set the packages to install to an empty slice config.OS.Packages.Install = []string{} + _, err = validateConfig(t.Context(), configFile, &config, rpmSources, toolsFile, outputImageFile, outputImageFormat, packageSnapshotTime, buildDir) assert.ErrorContains(t, err, "no packages to install specified, please specify at least one package to install for a new image") diff --git a/toolkit/tools/pkg/imagecustomizerlib/baseconfigs.go b/toolkit/tools/pkg/imagecustomizerlib/baseconfigs.go index e0dde4c140..20ce4b1991 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/baseconfigs.go +++ b/toolkit/tools/pkg/imagecustomizerlib/baseconfigs.go @@ -9,16 +9,21 @@ import ( "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/file" ) -type ConfigWithBasePath struct { +type ConfigWithPath struct { Config *imagecustomizerapi.Config - BaseConfigPath string + ConfigFilePath string } -func buildConfigChain(ctx context.Context, rc *ResolvedConfig) ([]*ConfigWithBasePath, error) { +// ConfigDir returns the directory containing the config file +func (c *ConfigWithPath) ConfigDir() string { + return filepath.Dir(c.ConfigFilePath) +} + +func buildConfigChain(ctx context.Context, rc *ResolvedConfig) ([]*ConfigWithPath, error) { visited := make(map[string]bool) pathStack := []string{} - configChain, err := buildConfigChainHelper(ctx, rc.Config, rc.BaseConfigPath, visited, pathStack) + configChain, err := buildConfigChainHelper(ctx, rc.Config, rc.ConfigFilePath, visited, pathStack) if err != nil { return nil, err } @@ -26,10 +31,12 @@ func buildConfigChain(ctx context.Context, rc *ResolvedConfig) ([]*ConfigWithBas return configChain, nil } -func buildConfigChainHelper(ctx context.Context, cfg *imagecustomizerapi.Config, configDir string, - visited map[string]bool, pathStack []string) ([]*ConfigWithBasePath, error) { +func buildConfigChainHelper(ctx context.Context, cfg *imagecustomizerapi.Config, configFilePath string, + visited map[string]bool, pathStack []string) ([]*ConfigWithPath, error) { + + var chain []*ConfigWithPath - var chain []*ConfigWithBasePath + configDir := filepath.Dir(configFilePath) for _, base := range cfg.BaseConfigs { // Resolve base config path relative to current config's directory @@ -54,8 +61,7 @@ func buildConfigChainHelper(ctx context.Context, cfg *imagecustomizerapi.Config, } // Recurse into base config - baseConfigDir := filepath.Dir(absPath) - subChain, err := buildConfigChainHelper(ctx, &baseCfg, baseConfigDir, visited, pathStack) + subChain, err := buildConfigChainHelper(ctx, &baseCfg, absPath, visited, pathStack) if err != nil { return nil, err } @@ -64,9 +70,9 @@ func buildConfigChainHelper(ctx context.Context, cfg *imagecustomizerapi.Config, } // Add the current config last - chain = append(chain, &ConfigWithBasePath{ + chain = append(chain, &ConfigWithPath{ Config: cfg, - BaseConfigPath: configDir, + ConfigFilePath: configFilePath, }) return chain, nil diff --git a/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go b/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go index 5aaba7de93..3efddb20d9 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/testutils" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/userutils" "github.com/stretchr/testify/assert" + "golang.org/x/sys/unix" ) func TestBaseConfigsInputAndOutput(t *testing.T) { @@ -31,7 +32,7 @@ func TestBaseConfigsInputAndOutput(t *testing.T) { err := imagecustomizerapi.UnmarshalYamlFile(currentConfigFile, &config) assert.NoError(t, err) - rc, err := ValidateConfig(t.Context(), testDir, &config, false, options) + rc, err := ValidateConfig(t.Context(), currentConfigFile, &config, false, options) assert.NoError(t, err) // Verify resolved values @@ -74,18 +75,31 @@ func TestBaseConfigsFullRun(t *testing.T) { testTmpDir := filepath.Join(tmpDir, "TestBaseConfigsFullRun") defer os.RemoveAll(testTmpDir) - buildDir := filepath.Join(testTmpDir, "build") outImageFilePath := filepath.Join(testTmpDir, "image.raw") - currentConfigFile := filepath.Join(testDir, "hierarchical-config.yaml") + options := ImageCustomizerOptions{ + BuildDir: buildDir, + UseBaseImageRpmRepos: true, + } + + var config imagecustomizerapi.Config + err = imagecustomizerapi.UnmarshalYamlFile(currentConfigFile, &config) + assert.NoError(t, err) + + rc, err := ValidateConfig(t.Context(), currentConfigFile, &config, false, options) + assert.NoError(t, err) + + // Verify VerityPartitionsType is correctly resolved + assert.Equal(t, imagecustomizerapi.VerityPartitionsUsesConfig, rc.Storage.VerityPartitionsType, + "VerityPartitionsType should be VerityPartitionsUsesConfig since the top-level config defines verity partitions") + err = CustomizeImageWithConfigFile(t.Context(), buildDir, currentConfigFile, baseImage, nil, outImageFilePath, "raw", true, "") if !assert.NoError(t, err) { return } - assert.FileExists(t, outImageFilePath) mountPoints := []testutils.MountPoint{ @@ -93,6 +107,7 @@ func TestBaseConfigsFullRun(t *testing.T) { PartitionNum: 3, Path: "/", FileSystemType: "ext4", + Flags: unix.MS_RDONLY, }, { PartitionNum: 2, @@ -244,4 +259,77 @@ func TestBaseConfigsFullRun(t *testing.T) { assert.NoError(t, err) assert.NotContains(t, grubCfgContents, "rd.info") assert.Contains(t, grubCfgContents, "console=tty0 console=ttyS0") + + partitions, err := getDiskPartitionsMap(imageConnection.Loopback().DevicePath()) + assert.NoError(t, err, "get disk partitions") + + // Verify partitions + assert.Len(t, partitions, 4, "should have 4 partitions from top-level config") + expectedLabels := []string{"esp", "boot", "root", "roothash"} + actualLabels := []string{partitions[1].PartLabel, partitions[2].PartLabel, + partitions[3].PartLabel, partitions[4].PartLabel} + assert.ElementsMatch(t, expectedLabels, actualLabels, + "partition labels should match top-level config (esp, boot, root, roothash)") + + // Verify verity + bootPath := filepath.Join(imageConnection.Chroot().RootDir(), "/boot") + rootDevice := testutils.PartitionDevPath(imageConnection, 3) + hashDevice := testutils.PartitionDevPath(imageConnection, 4) + + verifyVerityGrub(t, bootPath, rootDevice, hashDevice, "PARTUUID="+partitions[3].PartUuid, + "PARTUUID="+partitions[4].PartUuid, "root", "console=tty0", baseImageInfo, "panic-on-corruption", + ) + +} + +func TestValidateDisksThenResetUUID(t *testing.T) { + testTempDir := filepath.Join(tmpDir, "TestValidateDisksThenResetUUID") + defer os.RemoveAll(testTempDir) + + buildDir := filepath.Join(testTempDir, "build") + currentConfigFile := filepath.Join(testDir, "hierarchical-config-storage-resetuuid.yaml") + + var config imagecustomizerapi.Config + err := imagecustomizerapi.UnmarshalYamlFile(currentConfigFile, &config) + assert.NoError(t, err) + + absCurrentConfigFile, err := filepath.Abs(currentConfigFile) + assert.NoError(t, err) + + options := ImageCustomizerOptions{ + BuildDir: buildDir, + } + + _, err = ValidateConfig(t.Context(), absCurrentConfigFile, &config, false, options) + + assert.Error(t, err) + assert.ErrorContains(t, err, "cannot specify 'resetPartitionsUuidsType'") + assert.ErrorContains(t, err, "hierarchical-config-storage-resetuuid.yaml") + assert.ErrorContains(t, err, "when a base config specifies '.storage.disks'") +} + +func TestValidateDisksThenReinitVerity(t *testing.T) { + testTempDir := filepath.Join(tmpDir, "TestValidateDisksThenReinitVerity") + defer os.RemoveAll(testTempDir) + + buildDir := filepath.Join(testTempDir, "build") + currentConfigFile := filepath.Join(testDir, "hierarchical-config-storage-reinitverity.yaml") + + var config imagecustomizerapi.Config + err := imagecustomizerapi.UnmarshalYamlFile(currentConfigFile, &config) + assert.NoError(t, err) + + absCurrentConfigFile, err := filepath.Abs(currentConfigFile) + assert.NoError(t, err) + + options := ImageCustomizerOptions{ + BuildDir: buildDir, + } + + _, err = ValidateConfig(t.Context(), absCurrentConfigFile, &config, false, options) + + assert.Error(t, err) + assert.ErrorContains(t, err, "cannot specify 'reinitializeVerity'") + assert.ErrorContains(t, err, "hierarchical-config-storage-reinitverity.yaml") + assert.ErrorContains(t, err, "when a base config specifies '.storage.disks'") } diff --git a/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go b/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go index 3cce8d8436..ea2a8497d2 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go +++ b/toolkit/tools/pkg/imagecustomizerlib/configvalidation.go @@ -46,14 +46,14 @@ var ( ErrOutputSelinuxPolicyPathIsFileConfig = NewImageCustomizerError("Validation:OutputSelinuxPolicyPathIsFileConfig", "path exists but is not a directory") ) -func ValidateConfig(ctx context.Context, baseConfigPath string, config *imagecustomizerapi.Config, +func ValidateConfig(ctx context.Context, configFilePath string, config *imagecustomizerapi.Config, newImage bool, options ImageCustomizerOptions, ) (*ResolvedConfig, error) { _, span := otel.GetTracerProvider().Tracer(OtelTracerName).Start(ctx, "validate_config") defer span.End() rc := &ResolvedConfig{ - BaseConfigPath: baseConfigPath, + ConfigFilePath: configFilePath, Config: config, Options: options, } @@ -78,10 +78,17 @@ func ValidateConfig(ctx context.Context, baseConfigPath string, config *imagecus return nil, err } - rc.CustomizeOSPartitions = config.CustomizePartitions() || - config.OS != nil || - len(config.Scripts.PostCustomization) > 0 || - len(config.Scripts.FinalizeCustomization) > 0 + rc.CustomizeOSPartitions = false + + for _, configWithBase := range rc.ConfigChain { + if len(configWithBase.Config.Storage.Disks) > 0 || + configWithBase.Config.OS != nil || + len(configWithBase.Config.Scripts.PostCustomization) > 0 || + len(configWithBase.Config.Scripts.FinalizeCustomization) > 0 { + rc.CustomizeOSPartitions = true + break + } + } // Create a UUID for the image. rc.ImageUuid, rc.ImageUuidStr, err = randomization.CreateUuid() @@ -103,6 +110,8 @@ func ValidateConfig(ctx context.Context, baseConfigPath string, config *imagecus return nil, err } + baseConfigPath := rc.ConfigDir() + if !newImage { rc.InputImage, err = validateInput(rc.ConfigChain, options.InputImageFile, options.InputImage) if err != nil { @@ -120,8 +129,20 @@ func ValidateConfig(ctx context.Context, baseConfigPath string, config *imagecus return nil, err } + // Validate cross-config storage rules + err = validateStorage(rc.ConfigChain) + if err != nil { + return nil, fmt.Errorf("invalid storage inheritance:\n%w", err) + } + rc.Hostname = resolveHostname(rc.ConfigChain) rc.SELinux = resolveSeLinux(rc.ConfigChain) + + rc.Storage, err = resolveStorage(rc.ConfigChain) + if err != nil { + return nil, err + } + rc.BootLoader.ResetType = resolveBootLoaderResetType(rc.ConfigChain) rc.Uki = resolveUki(rc.ConfigChain) rc.KernelCommandLine = resolveKernelCommandLine(rc.ConfigChain) @@ -160,7 +181,7 @@ func ValidateConfigPostImageDownload(rc *ResolvedConfig) error { return nil } -func validateInput(configChain []*ConfigWithBasePath, inputImageFile string, inputImage string, +func validateInput(configChain []*ConfigWithPath, inputImageFile string, inputImage string, ) (imagecustomizerapi.InputImage, error) { if inputImageFile != "" { if yes, err := file.IsFile(inputImageFile); err != nil { @@ -189,7 +210,7 @@ func validateInput(configChain []*ConfigWithBasePath, inputImageFile string, inp for _, configWithBase := range slices.Backward(configChain) { if configWithBase.Config.Input.Image.Path != "" { inputImageAbsPath := file.GetAbsPathWithBase( - configWithBase.BaseConfigPath, + configWithBase.ConfigDir(), configWithBase.Config.Input.Image.Path, ) @@ -370,7 +391,7 @@ func validatePackageLists(baseConfigPath string, config *imagecustomizerapi.OS, return nil } -func validateOutputImageFormat(configChain []*ConfigWithBasePath, cliOutputImageFormat imagecustomizerapi.ImageFormatType, +func validateOutputImageFormat(configChain []*ConfigWithPath, cliOutputImageFormat imagecustomizerapi.ImageFormatType, ) (imagecustomizerapi.ImageFormatType, error) { if cliOutputImageFormat != "" { return cliOutputImageFormat, nil @@ -386,7 +407,7 @@ func validateOutputImageFormat(configChain []*ConfigWithBasePath, cliOutputImage return "", ErrOutputImageFormatRequired } -func validateOutputImageFile(configChain []*ConfigWithBasePath, cliOutputImageFile string, +func validateOutputImageFile(configChain []*ConfigWithPath, cliOutputImageFile string, outputImageFormat imagecustomizerapi.ImageFormatType, ) (string, error) { if cliOutputImageFile != "" { @@ -404,7 +425,7 @@ func validateOutputImageFile(configChain []*ConfigWithBasePath, cliOutputImageFi for _, configWithBase := range slices.Backward(configChain) { if configWithBase.Config.Output.Image.Path != "" { outputImageFile := file.GetAbsPathWithBase( - configWithBase.BaseConfigPath, + configWithBase.ConfigDir(), configWithBase.Config.Output.Image.Path, ) @@ -452,7 +473,7 @@ func validateUser(baseConfigPath string, user imagecustomizerapi.User) error { return nil } -func validateOutputSelinuxPolicyPath(configChain []*ConfigWithBasePath, cliOutputSelinuxPolicyPath string) (string, error) { +func validateOutputSelinuxPolicyPath(configChain []*ConfigWithPath, cliOutputSelinuxPolicyPath string) (string, error) { // CLI parameter takes precedence. if cliOutputSelinuxPolicyPath != "" { if isDir, err := file.DirExists(cliOutputSelinuxPolicyPath); err != nil { @@ -469,7 +490,7 @@ func validateOutputSelinuxPolicyPath(configChain []*ConfigWithBasePath, cliOutpu for _, configWithBase := range slices.Backward(configChain) { if configWithBase.Config.Output.SelinuxPolicyPath != "" { outputSelinuxPolicyPath := file.GetAbsPathWithBase( - configWithBase.BaseConfigPath, + configWithBase.ConfigDir(), configWithBase.Config.Output.SelinuxPolicyPath, ) @@ -503,7 +524,7 @@ func validateIsoPxeCustomization(rc *ResolvedConfig) error { // While defining a storage configuration can work when the input image is // an iso, there is no obvious point of moving content between partitions // where all partitions get collapsed into the squashfs at the end. - if rc.Config.CustomizePartitions() { + if rc.Storage.CustomizePartitions() { return ErrCannotCustomizePartitionsOnIso } } @@ -511,7 +532,67 @@ func validateIsoPxeCustomization(rc *ResolvedConfig) error { return nil } -func resolveOutputArtifacts(configChain []*ConfigWithBasePath) *imagecustomizerapi.Artifacts { +func validateStorage(configChain []*ConfigWithPath) error { + var baseHasDisks bool + + for _, configWithBase := range configChain { + storage := configWithBase.Config.Storage + + hasDisks := storage.CustomizePartitions() + hasResetUUID := storage.ResetPartitionsUuidsType != imagecustomizerapi.ResetPartitionsUuidsTypeDefault + hasReinitVerity := storage.ReinitializeVerity != imagecustomizerapi.ReinitializeVerityTypeDefault + + if baseHasDisks { + configFullPath := configWithBase.ConfigFilePath + if hasResetUUID { + return fmt.Errorf( + "cannot specify 'resetPartitionsUuidsType' in %s when a base config specifies '.storage.disks'", configFullPath) + } + if hasReinitVerity { + return fmt.Errorf( + "cannot specify 'reinitializeVerity' in %s when a base config specifies '.storage.disks'", configFullPath) + } + } + + // For next iteration + if hasDisks { + baseHasDisks = true + } + } + + return nil +} + +func resolveStorage(configChain []*ConfigWithPath) (imagecustomizerapi.Storage, error) { + var resolvedStorage imagecustomizerapi.Storage + + for _, configWithBase := range slices.Backward(configChain) { + storage := configWithBase.Config.Storage + + // If current config has disks, it overrides all base configs + if storage.CustomizePartitions() { + return storage, nil + } + + // Otherwise, accumulate individual override fields + if storage.ResetPartitionsUuidsType != imagecustomizerapi.ResetPartitionsUuidsTypeDefault { + resolvedStorage.ResetPartitionsUuidsType = storage.ResetPartitionsUuidsType + } + + if storage.ReinitializeVerity != imagecustomizerapi.ReinitializeVerityTypeDefault { + resolvedStorage.ReinitializeVerity = storage.ReinitializeVerity + } + + if len(storage.Verity) > 0 { + resolvedStorage.Verity = storage.Verity + resolvedStorage.VerityPartitionsType = storage.VerityPartitionsType + } + } + + return resolvedStorage, nil +} + +func resolveOutputArtifacts(configChain []*ConfigWithPath) *imagecustomizerapi.Artifacts { var artifacts *imagecustomizerapi.Artifacts for _, configWithBase := range configChain { @@ -523,7 +604,7 @@ func resolveOutputArtifacts(configChain []*ConfigWithBasePath) *imagecustomizera // Artifacts path from current config overrides previous one if configWithBase.Config.Output.Artifacts.Path != "" { artifacts.Path = file.GetAbsPathWithBase( - configWithBase.BaseConfigPath, + configWithBase.ConfigDir(), configWithBase.Config.Output.Artifacts.Path, ) } @@ -563,7 +644,7 @@ func mergeOutputArtifactTypes(base, current []imagecustomizerapi.OutputArtifacts return merged } -func resolveHostname(configChain []*ConfigWithBasePath) string { +func resolveHostname(configChain []*ConfigWithPath) string { for _, configWithBase := range slices.Backward(configChain) { if configWithBase.Config.OS != nil && configWithBase.Config.OS.Hostname != "" { return configWithBase.Config.OS.Hostname @@ -573,7 +654,7 @@ func resolveHostname(configChain []*ConfigWithBasePath) string { return "" } -func resolveSeLinux(configChain []*ConfigWithBasePath) imagecustomizerapi.SELinux { +func resolveSeLinux(configChain []*ConfigWithPath) imagecustomizerapi.SELinux { for _, configWithBase := range slices.Backward(configChain) { if configWithBase.Config.OS != nil && configWithBase.Config.OS.SELinux.Mode != "" { return configWithBase.Config.OS.SELinux @@ -582,7 +663,7 @@ func resolveSeLinux(configChain []*ConfigWithBasePath) imagecustomizerapi.SELinu return imagecustomizerapi.SELinux{} } -func resolveUki(configChain []*ConfigWithBasePath) *imagecustomizerapi.Uki { +func resolveUki(configChain []*ConfigWithPath) *imagecustomizerapi.Uki { for _, configWithBase := range slices.Backward(configChain) { if configWithBase.Config.OS != nil && configWithBase.Config.OS.Uki != nil { return configWithBase.Config.OS.Uki @@ -591,7 +672,7 @@ func resolveUki(configChain []*ConfigWithBasePath) *imagecustomizerapi.Uki { return nil } -func resolveBootLoaderResetType(configChain []*ConfigWithBasePath) imagecustomizerapi.ResetBootLoaderType { +func resolveBootLoaderResetType(configChain []*ConfigWithPath) imagecustomizerapi.ResetBootLoaderType { for _, cfg := range slices.Backward(configChain) { if cfg.Config.OS == nil { continue @@ -610,7 +691,7 @@ func resolveBootLoaderResetType(configChain []*ConfigWithBasePath) imagecustomiz return "" } -func resolveKernelCommandLine(configChain []*ConfigWithBasePath) imagecustomizerapi.KernelCommandLine { +func resolveKernelCommandLine(configChain []*ConfigWithPath) imagecustomizerapi.KernelCommandLine { var mergedArgs []string // Concatenate all kernel command line args diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizebootloader.go b/toolkit/tools/pkg/imagecustomizerlib/customizebootloader.go index 7e723d39b9..ec22ff4942 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizebootloader.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizebootloader.go @@ -35,8 +35,8 @@ func handleBootLoader(ctx context.Context, rc *ResolvedConfig, imageConnection * ) error { switch { case rc.BootLoader.ResetType == imagecustomizerapi.ResetBootLoaderTypeHard || newImage: - err := hardResetBootLoader(ctx, rc.BaseConfigPath, rc.Config, imageConnection, partitionsLayout, - newImage, rc.SELinux) + err := hardResetBootLoader(ctx, rc, imageConnection, partitionsLayout, + newImage) if err != nil { return fmt.Errorf("%w:\n%w", ErrBootloaderHardReset, err) } @@ -52,8 +52,8 @@ func handleBootLoader(ctx context.Context, rc *ResolvedConfig, imageConnection * return nil } -func hardResetBootLoader(ctx context.Context, baseConfigPath string, config *imagecustomizerapi.Config, imageConnection *imageconnection.ImageConnection, - partitionsLayout []fstabEntryPartNum, newImage bool, selinuxConfig imagecustomizerapi.SELinux, +func hardResetBootLoader(ctx context.Context, rc *ResolvedConfig, imageConnection *imageconnection.ImageConnection, + partitionsLayout []fstabEntryPartNum, newImage bool, ) error { var err error logger.Log.Infof("Hard reset bootloader config") @@ -78,8 +78,9 @@ func hardResetBootLoader(ctx context.Context, baseConfigPath string, config *ima var rootMountIdType imagecustomizerapi.MountIdentifierType var bootType imagecustomizerapi.BootType - if config.CustomizePartitions() { - rootFileSystem, foundRootFileSystem := sliceutils.FindValueFunc(config.Storage.FileSystems, + + if rc.Storage.CustomizePartitions() { + rootFileSystem, foundRootFileSystem := sliceutils.FindValueFunc(rc.Storage.FileSystems, func(fileSystem imagecustomizerapi.FileSystem) bool { return fileSystem.MountPoint != nil && fileSystem.MountPoint.Path == "/" @@ -90,7 +91,7 @@ func hardResetBootLoader(ctx context.Context, baseConfigPath string, config *ima } rootMountIdType = rootFileSystem.MountPoint.IdType - bootType = config.Storage.BootType + bootType = rc.Storage.BootType } else { rootMountIdType, err = findRootMountIdType(partitionsLayout) if err != nil { @@ -104,8 +105,8 @@ func hardResetBootLoader(ctx context.Context, baseConfigPath string, config *ima } // Hard-reset the grub config. - err = configureDiskBootLoader(imageConnection, rootMountIdType, bootType, selinuxConfig, - config.OS.KernelCommandLine, currentSelinuxMode, newImage) + err = configureDiskBootLoader(imageConnection, rootMountIdType, bootType, rc.SELinux, + rc.Config.OS.KernelCommandLine, currentSelinuxMode, newImage) if err != nil { return fmt.Errorf("%w:\n%w", ErrBootloaderDiskConfigure, err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go index 2b8d8ea997..04ca4b1aed 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeos.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeos.go @@ -35,7 +35,7 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection snapshotTime = rc.Options.PackageSnapshotTime } - err = addRemoveAndUpdatePackages(ctx, rc.BuildDirAbs, rc.BaseConfigPath, configWithBase.Config.OS, + err = addRemoveAndUpdatePackages(ctx, rc.BuildDirAbs, rc.ConfigDir(), configWithBase.Config.OS, imageChroot, nil, rc.Options.RpmsSources, rc.Options.UseBaseImageRpmRepos, distroHandler, snapshotTime) if err != nil { @@ -43,7 +43,7 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection } } - err = UpdateHostname(ctx, rc.Config.OS.Hostname, imageChroot) + err = UpdateHostname(ctx, rc.Hostname, imageChroot) if err != nil { return err } @@ -54,21 +54,21 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection return err } - err = AddOrUpdateUsers(ctx, configWithBase.Config.OS.Users, configWithBase.BaseConfigPath, imageChroot) + err = AddOrUpdateUsers(ctx, configWithBase.Config.OS.Users, configWithBase.ConfigDir(), imageChroot) if err != nil { return err } } for _, configWithBase := range rc.ConfigChain { - err = copyAdditionalDirs(ctx, configWithBase.BaseConfigPath, configWithBase.Config.OS.AdditionalDirs, imageChroot) + err = copyAdditionalDirs(ctx, configWithBase.ConfigDir(), configWithBase.Config.OS.AdditionalDirs, imageChroot) if err != nil { return err } } for _, configWithBase := range rc.ConfigChain { - err = copyAdditionalFiles(ctx, configWithBase.BaseConfigPath, configWithBase.Config.OS.AdditionalFiles, + err = copyAdditionalFiles(ctx, configWithBase.ConfigDir(), configWithBase.Config.OS.AdditionalFiles, imageChroot) if err != nil { return err @@ -95,7 +95,7 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection } if rc.Config.OS.ImageHistory != imagecustomizerapi.ImageHistoryNone { - err = addImageHistory(ctx, imageChroot, rc.ImageUuidStr, rc.BaseConfigPath, ToolVersion, buildTime, rc.Config) + err = addImageHistory(ctx, imageChroot, rc.ImageUuidStr, rc.ConfigDir(), ToolVersion, buildTime, rc.Config) if err != nil { return err } @@ -120,7 +120,7 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection overlayUpdated = overlayUpdated || updated } - verityUpdated, err := enableVerityPartition(ctx, rc.Config.Storage.Verity, imageChroot, distroHandler) + verityUpdated, err := enableVerityPartition(ctx, rc.Storage.Verity, imageChroot, distroHandler) if err != nil { return err } @@ -132,12 +132,12 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection } } - err = runUserScripts(ctx, rc.BaseConfigPath, rc.Config.Scripts.PostCustomization, "postCustomization", imageChroot) + err = runUserScripts(ctx, rc.ConfigDir(), rc.Config.Scripts.PostCustomization, "postCustomization", imageChroot) if err != nil { return err } - err = prepareUki(ctx, rc.BuildDirAbs, rc.Config.OS.Uki, imageChroot, distroHandler) + err = prepareUki(ctx, rc.BuildDirAbs, rc.Uki, imageChroot, distroHandler) if err != nil { return err } @@ -152,7 +152,7 @@ func doOsCustomizations(ctx context.Context, rc *ResolvedConfig, imageConnection return err } - err = runUserScripts(ctx, rc.BaseConfigPath, rc.Config.Scripts.FinalizeCustomization, "finalizeCustomization", + err = runUserScripts(ctx, rc.ConfigDir(), rc.Config.Scripts.FinalizeCustomization, "finalizeCustomization", imageChroot) if err != nil { return err diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizepartitions.go b/toolkit/tools/pkg/imagecustomizerlib/customizepartitions.go index 2ebf9f9cac..6f44b6e6a1 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizepartitions.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizepartitions.go @@ -20,22 +20,23 @@ var ( ErrPartitionsResetUuids = NewImageCustomizerError("Partitions:ResetUuids", "failed to reset partition UUIDs") ) -func customizePartitions(ctx context.Context, buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, - buildImageFile string, targetOS targetos.TargetOs, +func customizePartitions(ctx context.Context, rc *ResolvedConfig, targetOS targetos.TargetOs, ) (bool, string, map[string]string, error) { + + storage := rc.Storage + switch { - case config.CustomizePartitions(): + case storage.CustomizePartitions(): logger.Log.Infof("Customizing partitions") _, span := otel.GetTracerProvider().Tracer(OtelTracerName).Start(ctx, "customize_partitions") defer span.End() - newBuildImageFile := filepath.Join(buildDir, PartitionCustomizedImageName) + newBuildImageFile := filepath.Join(rc.BuildDirAbs, PartitionCustomizedImageName) // If there is no known way to create the new partition layout from the old one, // then fallback to creating the new partitions from scratch and doing a file copy. - partIdToPartUuid, err := customizePartitionsUsingFileCopy(ctx, buildDir, baseConfigPath, config, - buildImageFile, newBuildImageFile, targetOS) + partIdToPartUuid, err := customizePartitionsUsingFileCopy(ctx, rc, newBuildImageFile, targetOS) if err != nil { os.Remove(newBuildImageFile) return false, "", nil, fmt.Errorf("%w:\n%w", ErrPartitionsCustomize, err) @@ -43,17 +44,17 @@ func customizePartitions(ctx context.Context, buildDir string, baseConfigPath st return true, newBuildImageFile, partIdToPartUuid, nil - case config.Storage.ResetPartitionsUuidsType != imagecustomizerapi.ResetPartitionsUuidsTypeDefault: - err := resetPartitionsUuids(ctx, buildImageFile, buildDir) + case storage.ResetPartitionsUuidsType != imagecustomizerapi.ResetPartitionsUuidsTypeDefault: + err := resetPartitionsUuids(ctx, rc.RawImageFile, rc.BuildDirAbs) if err != nil { return false, "", nil, fmt.Errorf("%w:\n%w", ErrPartitionsResetUuids, err) } - return true, buildImageFile, nil, nil + return true, rc.RawImageFile, nil, nil default: // No changes to make to the partitions. // So, just use the original disk. - return false, buildImageFile, nil, nil + return false, rc.RawImageFile, nil, nil } } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go b/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go index 6d28226034..e5e076b146 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go @@ -7,7 +7,6 @@ import ( "context" "fmt" - "github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/safechroot" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/shell" "github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/targetos" @@ -21,24 +20,24 @@ var ( ErrPartitionCopyFiles = NewImageCustomizerError("PartitionCopy:Files", "failed to copy partition files") ) -func customizePartitionsUsingFileCopy(ctx context.Context, buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, - buildImageFile string, newBuildImageFile string, targetOS targetos.TargetOs, +func customizePartitionsUsingFileCopy(ctx context.Context, rc *ResolvedConfig, + newBuildImageFile string, targetOS targetos.TargetOs, ) (map[string]string, error) { - existingImageConnection, _, _, _, err := connectToExistingImage(ctx, buildImageFile, buildDir, "imageroot", false, + existingImageConnection, _, _, _, err := connectToExistingImage(ctx, rc.RawImageFile, rc.BuildDirAbs, "imageroot", false, true, false, false) if err != nil { return nil, err } defer existingImageConnection.Close() - diskConfig := config.Storage.Disks[0] + diskConfig := rc.Storage.Disks[0] installOSFunc := func(imageChroot *safechroot.Chroot) error { return copyFilesIntoNewDisk(existingImageConnection.Chroot(), imageChroot) } - partIdToPartUuid, err := CreateNewImage(targetOS, newBuildImageFile, diskConfig, config.Storage.FileSystems, - buildDir, "newimageroot", installOSFunc) + partIdToPartUuid, err := CreateNewImage(targetOS, newBuildImageFile, diskConfig, rc.Storage.FileSystems, + rc.BuildDirAbs, "newimageroot", installOSFunc) if err != nil { return nil, err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go b/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go index 5abb7ac189..8a4370d068 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizeverity.go @@ -469,10 +469,10 @@ func updateUkiKernelArgsForVerity(verityMetadata []verityDeviceMetadata, return nil } -func validateVerityMountPaths(imageConnection *imageconnection.ImageConnection, config *imagecustomizerapi.Config, +func validateVerityMountPaths(imageConnection *imageconnection.ImageConnection, storage imagecustomizerapi.Storage, partitionsLayout []fstabEntryPartNum, baseImageVerityMetadata []verityDeviceMetadata, ) error { - if config.Storage.VerityPartitionsType != imagecustomizerapi.VerityPartitionsUsesExisting { + if storage.VerityPartitionsType != imagecustomizerapi.VerityPartitionsUsesExisting { // Either: // - Verity is not being used OR // - Partitions were customized and the verity checks were done during the API validity checks. @@ -486,8 +486,8 @@ func validateVerityMountPaths(imageConnection *imageconnection.ImageConnection, } verityDeviceMountPoint := make(map[*imagecustomizerapi.Verity]*imagecustomizerapi.MountPoint) - for i := range config.Storage.Verity { - verity := &config.Storage.Verity[i] + for i := range storage.Verity { + verity := &storage.Verity[i] dataPartition, err := findIdentifiedPartition(partitions, *verity.DataDevice) if err != nil { @@ -534,7 +534,7 @@ func validateVerityMountPaths(imageConnection *imageconnection.ImageConnection, verityDeviceMountPoint[verity] = mountPoint } - err = imagecustomizerapi.ValidateVerityMounts(config.Storage.Verity, verityDeviceMountPoint) + err = imagecustomizerapi.ValidateVerityMounts(storage.Verity, verityDeviceMountPoint) if err != nil { return err } @@ -574,9 +574,8 @@ func findIdentifiedPartition(partitions []diskutils.PartitionInfo, ref imagecust return partition, nil } -func customizeVerityImageHelper(ctx context.Context, buildDir string, config *imagecustomizerapi.Config, - buildImageFile string, partIdToPartUuid map[string]string, shrinkHashPartition bool, - baseImageVerity []verityDeviceMetadata, readonlyPartUuids []string, +func customizeVerityImageHelper(ctx context.Context, rc *ResolvedConfig, partIdToPartUuid map[string]string, + shrinkHashPartition bool, baseImageVerity []verityDeviceMetadata, readonlyPartUuids []string, partitionsLayout []fstabEntryPartNum, ) ([]verityDeviceMetadata, error) { logger.Log.Infof("Provisioning verity") @@ -586,7 +585,10 @@ func customizeVerityImageHelper(ctx context.Context, buildDir string, config *im verityMetadata := []verityDeviceMetadata(nil) - loopback, err := safeloopback.NewLoopback(buildImageFile) + isUki := rc.Uki != nil + storage := rc.Storage + + loopback, err := safeloopback.NewLoopback(rc.RawImageFile) if err != nil { return nil, fmt.Errorf("%w:\n%w", ErrVerityImageConnection, err) } @@ -636,7 +638,7 @@ func customizeVerityImageHelper(ctx context.Context, buildDir string, config *im verityMetadata = append(verityMetadata, newMetadata) } - for _, verityConfig := range config.Storage.Verity { + for _, verityConfig := range storage.Verity { // Extract the partition block device path. dataPartition, err := verityIdToPartition(verityConfig.DataDeviceId, verityConfig.DataDevice, partIdToPartUuid, diskPartitions) @@ -683,8 +685,7 @@ func customizeVerityImageHelper(ctx context.Context, buildDir string, config *im } // Update kernel args. - isUki := config.OS.Uki != nil - err = updateKernelArgsForVerity(buildDir, diskPartitions, verityMetadata, isUki, partitionsLayout) + err = updateKernelArgsForVerity(rc.BuildDirAbs, diskPartitions, verityMetadata, isUki, partitionsLayout) if err != nil { return nil, err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecreator.go b/toolkit/tools/pkg/imagecustomizerlib/imagecreator.go index 78ecfb1b70..19fbd14abc 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecreator.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecreator.go @@ -18,7 +18,7 @@ import ( func CustomizeImageHelperImageCreator(ctx context.Context, rc *ResolvedConfig, tarFile string, distroHandler distroHandler, ) ([]fstabEntryPartNum, string, error) { - logger.Log.Debugf("Customizing OS image with config file %s", rc.BaseConfigPath) + logger.Log.Debugf("Customizing OS image with config file %s", rc.ConfigFilePath) toolsChrootDir := filepath.Join(rc.BuildDirAbs, toolsRoot) toolsChroot := safechroot.NewChroot(toolsChrootDir, false) @@ -87,7 +87,7 @@ func doOsCustomizationsImageCreator( snapshotTime = rc.Options.PackageSnapshotTime } - err = addRemoveAndUpdatePackages(ctx, rc.BuildDirAbs, rc.BaseConfigPath, configWithBase.Config.OS, + err = addRemoveAndUpdatePackages(ctx, rc.BuildDirAbs, rc.ConfigDir(), configWithBase.Config.OS, imageChroot, toolsChroot, rc.Options.RpmsSources, rc.Options.UseBaseImageRpmRepos, distroHandler, snapshotTime) if err != nil { @@ -95,7 +95,7 @@ func doOsCustomizationsImageCreator( } } - if err = UpdateHostname(ctx, rc.Config.OS.Hostname, imageChroot); err != nil { + if err = UpdateHostname(ctx, rc.Hostname, imageChroot); err != nil { return err } @@ -115,7 +115,7 @@ func doOsCustomizationsImageCreator( return fmt.Errorf("failed to clear systemd state:\n%w", err) } - err = runUserScripts(ctx, rc.BaseConfigPath, rc.Config.Scripts.PostCustomization, "postCustomization", imageChroot) + err = runUserScripts(ctx, rc.ConfigDir(), rc.Config.Scripts.PostCustomization, "postCustomization", imageChroot) if err != nil { return err } @@ -128,7 +128,7 @@ func doOsCustomizationsImageCreator( return err } - err = runUserScripts(ctx, rc.BaseConfigPath, rc.Config.Scripts.FinalizeCustomization, "finalizeCustomization", imageChroot) + err = runUserScripts(ctx, rc.ConfigDir(), rc.Config.Scripts.FinalizeCustomization, "finalizeCustomization", imageChroot) if err != nil { return err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index 5cc3c6b544..724e8b2937 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -153,7 +153,12 @@ func CustomizeImageWithConfigFileOptions(ctx context.Context, configFile string, return fmt.Errorf("%w:\n%w", ErrGetAbsoluteConfigPath, err) } - err = CustomizeImageOptions(ctx, absBaseConfigPath, &config, options) + absConfigFile, err := filepath.Abs(configFile) + if err != nil { + return fmt.Errorf("%w:\n%w", ErrGetAbsoluteConfigPath, err) + } + + err = CustomizeImageOptions(ctx, absBaseConfigPath, absConfigFile, &config, options) if err != nil { return err } @@ -174,7 +179,7 @@ func CustomizeImage(ctx context.Context, buildDir string, baseConfigPath string, inputImageFile string, rpmsSources []string, outputImageFile string, outputImageFormat string, useBaseImageRpmRepos bool, packageSnapshotTime string, ) (err error) { - return CustomizeImageOptions(ctx, baseConfigPath, config, ImageCustomizerOptions{ + return CustomizeImageOptions(ctx, baseConfigPath, "", config, ImageCustomizerOptions{ BuildDir: buildDir, InputImageFile: inputImageFile, RpmsSources: rpmsSources, @@ -185,7 +190,7 @@ func CustomizeImage(ctx context.Context, buildDir string, baseConfigPath string, }) } -func CustomizeImageOptions(ctx context.Context, baseConfigPath string, config *imagecustomizerapi.Config, +func CustomizeImageOptions(ctx context.Context, baseConfigPath string, configFilePath string, config *imagecustomizerapi.Config, options ImageCustomizerOptions, ) (err error) { ctx, span := otel.GetTracerProvider().Tracer(OtelTracerName).Start(ctx, "customize_image") @@ -209,7 +214,7 @@ func CustomizeImageOptions(ctx context.Context, baseConfigPath string, config *i span.End() }() - rc, err := ValidateConfig(ctx, baseConfigPath, config, false, options) + rc, err := ValidateConfig(ctx, configFilePath, config, false, options) if err != nil { return fmt.Errorf("%w:\n%w", ErrInvalidImageConfig, err) } @@ -280,7 +285,7 @@ func CustomizeImageOptions(ctx context.Context, baseConfigPath string, config *i } if rc.OutputArtifacts != nil { - outputDir := file.GetAbsPathWithBase(baseConfigPath, rc.OutputArtifacts.Path) + outputDir := file.GetAbsPathWithBase(rc.ConfigDir(), rc.OutputArtifacts.Path) err = outputArtifacts(ctx, rc.OutputArtifacts.Items, outputDir, rc.BuildDirAbs, rc.RawImageFile, im.verityMetadata) @@ -485,8 +490,7 @@ func customizeOSContents(ctx context.Context, rc *ResolvedConfig) (imageMetadata im.targetOS = targetOS // Customize the partitions. - partitionsCustomized, newRawImageFile, partIdToPartUuid, err := customizePartitions(ctx, rc.BuildDirAbs, - rc.BaseConfigPath, rc.Config, rc.RawImageFile, im.targetOS) + partitionsCustomized, newRawImageFile, partIdToPartUuid, err := customizePartitions(ctx, rc, im.targetOS) if err != nil { return im, err } @@ -524,10 +528,10 @@ func customizeOSContents(ctx context.Context, rc *ResolvedConfig) (imageMetadata } } - if len(rc.Config.Storage.Verity) > 0 || len(im.baseImageVerityMetadata) > 0 { + if len(rc.Storage.Verity) > 0 || len(im.baseImageVerityMetadata) > 0 { // Customize image for dm-verity, setting up verity metadata and security features. - verityMetadata, err := customizeVerityImageHelper(ctx, rc.BuildDirAbs, rc.Config, rc.RawImageFile, - partIdToPartUuid, shrinkPartitions, im.baseImageVerityMetadata, readonlyPartUuids, partitionsLayout) + verityMetadata, err := customizeVerityImageHelper(ctx, rc, partIdToPartUuid, shrinkPartitions, + im.baseImageVerityMetadata, readonlyPartUuids, partitionsLayout) if err != nil { return im, fmt.Errorf("%w:\n%w", ErrCustomizeProvisionVerity, err) } @@ -618,13 +622,13 @@ func convertWriteableFormatToOutputImage(ctx context.Context, rc *ResolvedConfig // Either re-build the full OS image, or just re-package the existing one if rebuildFullOsImage { requestedSELinuxMode := rc.SELinux.Mode - err := createLiveOSFromRaw(ctx, rc.BuildDirAbs, rc.BaseConfigPath, inputIsoArtifacts, requestedSELinuxMode, + err := createLiveOSFromRaw(ctx, rc.BuildDirAbs, rc.ConfigDir(), inputIsoArtifacts, requestedSELinuxMode, rc.Config.Iso, rc.Config.Pxe, rc.RawImageFile, rc.OutputImageFormat, rc.OutputImageFile) if err != nil { return fmt.Errorf("%w:\n%w", ErrCreateLiveOSArtifacts, err) } } else { - err := repackageLiveOS(rc.BuildDirAbs, rc.BaseConfigPath, rc.Config.Iso, rc.Config.Pxe, + err := repackageLiveOS(rc.BuildDirAbs, rc.ConfigDir(), rc.Config.Iso, rc.Config.Pxe, inputIsoArtifacts, rc.OutputImageFormat, rc.OutputImageFile) if err != nil { return fmt.Errorf("%w:\n%w", ErrCreateLiveOSArtifacts, err) @@ -678,7 +682,7 @@ func customizeImageHelper(ctx context.Context, rc *ResolvedConfig, partitionsCus ) ([]fstabEntryPartNum, []verityDeviceMetadata, []string, string, error) { logger.Log.Debugf("Customizing OS") - readOnlyVerity := rc.Config.Storage.ReinitializeVerity != imagecustomizerapi.ReinitializeVerityTypeAll + readOnlyVerity := rc.Storage.ReinitializeVerity != imagecustomizerapi.ReinitializeVerityTypeAll imageConnection, partitionsLayout, baseImageVerityMetadata, readonlyPartUuids, err := connectToExistingImage( ctx, rc.RawImageFile, rc.BuildDirAbs, "imageroot", true, false, readOnlyVerity, false) @@ -702,7 +706,7 @@ func customizeImageHelper(ctx context.Context, rc *ResolvedConfig, partitionsCus return nil }) - err = validateVerityMountPaths(imageConnection, rc.Config, partitionsLayout, baseImageVerityMetadata) + err = validateVerityMountPaths(imageConnection, rc.Storage, partitionsLayout, baseImageVerityMetadata) if err != nil { return nil, nil, nil, "", fmt.Errorf("%w:\n%w", ErrVerityValidation, err) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer_test.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer_test.go index 2d06408526..f1e8877343 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer_test.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer_test.go @@ -138,7 +138,7 @@ func TestValidateConfig_CallsValidateInput(t *testing.T) { // Test that the input is being validated in validateConfig by // triggering an error in validateInput. - _, err := ValidateConfig(t.Context(), testDir, config, false, + _, err := ValidateConfig(t.Context(), "", config, false, ImageCustomizerOptions{ OutputImageFile: "./out/image.vhdx", OutputImageFormat: "vhdx", @@ -152,7 +152,7 @@ func TestValidateConfig_CallsValidateInput_NewImage(t *testing.T) { // Test that the input is being validated in validateConfig by // triggering an error in validateInput. - _, err := ValidateConfig(t.Context(), testDir, config, true, + _, err := ValidateConfig(t.Context(), "", config, true, ImageCustomizerOptions{ OutputImageFile: "./out/image.raw", OutputImageFormat: "raw", @@ -167,6 +167,9 @@ func TestValidateInput_AcceptsValidPaths(t *testing.T) { baseConfigPath := testDir config := &imagecustomizerapi.Config{} + // Use a config file path in testDir so relative paths resolve correctly + configFilePath := filepath.Join(baseConfigPath, "dummy-config.yaml") + inputImageFileFake := filepath.Join(testDir, "testimages", "doesnotexist.xxx") inputImageFileReal := filepath.Join(testDir, "testimages", "empty.vhdx") inputImageFileRealRelativeCwd, err := filepath.Rel(cwd, inputImageFileReal) @@ -184,19 +187,19 @@ func TestValidateInput_AcceptsValidPaths(t *testing.T) { } // The input image file can be specified as an argument without being specified in the config. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) options.InputImageFile = inputImageFileRealRelativeCwd // The input image file specified as an argument can be relative to the current working directory. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) options.InputImageFile = inputImageFileFake // The input image file, specified as an argument, must be a file. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), "", config, false, options) assert.Error(t, err) assert.ErrorContains(t, err, "doesnotexist.xxx: no such file or directory") @@ -204,19 +207,19 @@ func TestValidateInput_AcceptsValidPaths(t *testing.T) { config.Input.Image.Path = inputImageFileReal // The input image file can be specified in the config without being specified as an argument. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) config.Input.Image.Path = inputImageFileRealRelativeConfig // The input image file specified in the config can be relative to the bash config path. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) config.Input.Image.Path = inputImageFileFake // The input image file, specified in the config, must be a file. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.Error(t, err) assert.ErrorContains(t, err, "doesnotexist.xxx: no such file or directory") @@ -224,18 +227,19 @@ func TestValidateInput_AcceptsValidPaths(t *testing.T) { config.Input.Image.Path = inputImageFileReal // The input image file can be specified both as an argument and in the config. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) config.Input.Image.Path = inputImageFileFake // The input image file can even be invalid in the config if it is specified as an argument. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) } func TestValidateConfigValidAdditionalFiles(t *testing.T) { - _, err := ValidateConfig(t.Context(), testDir, + configFilePath := filepath.Join(testDir, "dummy-config.yaml") + _, err := ValidateConfig(t.Context(), configFilePath, &imagecustomizerapi.Config{ OS: &imagecustomizerapi.OS{ AdditionalFiles: imagecustomizerapi.AdditionalFileList{ @@ -260,7 +264,8 @@ func TestValidateConfigValidAdditionalFiles(t *testing.T) { } func TestValidateConfigMissingAdditionalFiles(t *testing.T) { - _, err := ValidateConfig(t.Context(), testDir, + configFilePath := filepath.Join(testDir, "dummy-config.yaml") + _, err := ValidateConfig(t.Context(), configFilePath, &imagecustomizerapi.Config{ OS: &imagecustomizerapi.OS{ AdditionalFiles: imagecustomizerapi.AdditionalFileList{ @@ -284,7 +289,8 @@ func TestValidateConfigMissingAdditionalFiles(t *testing.T) { } func TestValidateConfigdditionalFilesIsDir(t *testing.T) { - _, err := ValidateConfig(t.Context(), testDir, + configFilePath := filepath.Join(testDir, "dummy-config.yaml") + _, err := ValidateConfig(t.Context(), configFilePath, &imagecustomizerapi.Config{ OS: &imagecustomizerapi.OS{ AdditionalFiles: imagecustomizerapi.AdditionalFileList{ @@ -335,7 +341,6 @@ func TestValidateConfigScriptNonLocalFile(t *testing.T) { } func TestValidateConfig_CallsValidateOutput(t *testing.T) { - baseConfigPath := testDir config := &imagecustomizerapi.Config{ Input: imagecustomizerapi.Input{ Image: imagecustomizerapi.InputImage{ @@ -347,8 +352,11 @@ func TestValidateConfig_CallsValidateOutput(t *testing.T) { OutputImageFormat: imagecustomizerapi.ImageFormatTypeVhd, } + // Use a dummy config file path so relative input image path resolves correctly + configFilePath := filepath.Join(testDir, "dummy-config.yaml") + // Test that the output is being validated in validateConfig by triggering an error in validateOutput. - _, err := ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err := ValidateConfig(t.Context(), configFilePath, config, false, options) assert.Error(t, err) assert.ErrorContains(t, err, "output image file must be specified") } @@ -373,6 +381,9 @@ func TestValidateOutput_AcceptsValidPaths(t *testing.T) { }, } + // Use a dummy config file path so relative paths are resolved correctly + configFilePath := filepath.Join(baseConfigPath, "dummy-config.yaml") + options := ImageCustomizerOptions{} outputImageDir := filepath.Join(testTempDir, "out") @@ -401,91 +412,91 @@ func TestValidateOutput_AcceptsValidPaths(t *testing.T) { options.OutputImageFormat = imagecustomizerapi.ImageFormatType(filepath.Ext(options.OutputImageFile)[1:]) // The output image file can be sepcified as an argument without being in specified the config. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) options.OutputImageFile = outputImageFileNewRelativeCwd // The output image file can be specified as an argument relative to the current working directory. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) options.OutputImageFile = outputImageDir // The output image file, specified as an argument, must not be a directory. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.Error(t, err) assert.ErrorContains(t, err, "is a directory") options.OutputImageFile = outputImageDirRelativeCwd // The above is also true for relative paths. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.Error(t, err) assert.ErrorContains(t, err, "is a directory") options.OutputImageFile = outputImageFileExists // The output image file, specified as an argument, may be a file that already exists. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) options.OutputImageFile = outputImageFileExistsRelativeCwd // The above is also true for relative paths. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) options.OutputImageFile = "" config.Output.Image.Path = outputImageFileNew // The output image file cab be specified in the config without being specified as an argument. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) config.Output.Image.Path = outputImageFileNewRelativeConfig // The output image file can be specified in the config relative to the base config path. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) config.Output.Image.Path = outputImageDir // The output image file, specified in the config, must not be a directory. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.Error(t, err) assert.ErrorContains(t, err, "is a directory") config.Output.Image.Path = outputImageDirRelativeConfig // The above is also true for relative paths. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.Error(t, err) assert.ErrorContains(t, err, "is a directory") config.Output.Image.Path = outputImageFileExists // The output image file, specified in the config, may be a file that already exists. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) config.Output.Image.Path = outputImageFileExistsRelativeConfig // The above is also true for relative paths. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) options.OutputImageFile = outputImageFileNew config.Output.Image.Path = outputImageFileNew // The output image file can be specified both as an argument and in the config. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) config.Output.Image.Path = outputImageDir // The output image file can even be invalid in the config if it is specified as an argument. - _, err = ValidateConfig(t.Context(), baseConfigPath, config, false, options) + _, err = ValidateConfig(t.Context(), configFilePath, config, false, options) assert.NoError(t, err) } @@ -833,7 +844,6 @@ func TestValidateConfig_InputImageFileSelection(t *testing.T) { assert.NoError(t, err) // Pass the input image file only in the config. - configPath := "config.yaml" config := &imagecustomizerapi.Config{ Input: imagecustomizerapi.Input{ Image: imagecustomizerapi.InputImage{ @@ -848,7 +858,7 @@ func TestValidateConfig_InputImageFileSelection(t *testing.T) { } // The input image file should be set to the value in the config. - rc, err := ValidateConfig(t.Context(), configPath, config, false, options) + rc, err := ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.InputImage.Path, inputImageFileAsConfig) assert.Equal(t, rc.InputFileExt(), "vhdx") @@ -859,7 +869,7 @@ func TestValidateConfig_InputImageFileSelection(t *testing.T) { options.InputImageFile = inputImageFileAsArg // The input image file should be set to the value passed as an argument. - rc, err = ValidateConfig(t.Context(), configPath, config, false, options) + rc, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.InputImage.Path, inputImageFileAsArg) assert.Equal(t, rc.InputFileExt(), "vhdx") @@ -869,7 +879,7 @@ func TestValidateConfig_InputImageFileSelection(t *testing.T) { config.Input.Image.Path = inputImageFileAsConfig // The input image file should be set to the value passed as an argument. - rc, err = ValidateConfig(t.Context(), configPath, config, false, options) + rc, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.InputImage.Path, inputImageFileAsArg) assert.Equal(t, rc.InputFileExt(), "vhdx") @@ -879,7 +889,7 @@ func TestValidateConfig_InputImageFileSelection(t *testing.T) { options.InputImageFile = inputImageFileIsoAsArg options.OutputImageFormat = "iso" options.OutputImageFile = "out/image.iso" - rc, err = ValidateConfig(t.Context(), configPath, config, false, options) + rc, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.InputImage.Path, inputImageFileIsoAsArg) assert.Equal(t, rc.InputFileExt(), "iso") @@ -901,7 +911,6 @@ func TestValidateConfig_OutputImageFileSelection(t *testing.T) { err = file.Write("", inputImageFile) assert.NoError(t, err) - configPath := "config.yaml" config := &imagecustomizerapi.Config{} options := ImageCustomizerOptions{ @@ -911,14 +920,14 @@ func TestValidateConfig_OutputImageFileSelection(t *testing.T) { } // The output image file is not specified in the config or as an argument, so the output image file will be empty. - rc, err := ValidateConfig(t.Context(), configPath, config, false, options) + rc, err := ValidateConfig(t.Context(), "", config, false, options) assert.ErrorContains(t, err, "output image file must be specified") // Pass the output image file only in the config. config.Output.Image.Path = outputImageFilePathAsConfig // The output image file should be set to the value in the config. - rc, err = ValidateConfig(t.Context(), configPath, config, false, options) + rc, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.OutputImageFile, outputImageFilePathAsConfig) @@ -927,7 +936,7 @@ func TestValidateConfig_OutputImageFileSelection(t *testing.T) { options.OutputImageFile = outputImageFilePathAsArg // The output image file should be set to the value passed as an argument. - rc, err = ValidateConfig(t.Context(), configPath, config, false, options) + rc, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.OutputImageFile, outputImageFilePathAsArg) @@ -936,7 +945,7 @@ func TestValidateConfig_OutputImageFileSelection(t *testing.T) { // The output image file should be set to the value passed as an // argument. - rc, err = ValidateConfig(t.Context(), configPath, config, false, options) + rc, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.OutputImageFile, outputImageFilePathAsArg) } @@ -956,7 +965,6 @@ func TestValidateConfig_OutputImageFormatSelection(t *testing.T) { err = file.Write("", inputImageFile) assert.NoError(t, err) - configPath := "config.yaml" config := &imagecustomizerapi.Config{} options := ImageCustomizerOptions{ @@ -967,14 +975,14 @@ func TestValidateConfig_OutputImageFormatSelection(t *testing.T) { // The output image format is not specified in the config or as an // argument, so an error will be reported. - rc, err := ValidateConfig(t.Context(), configPath, config, false, options) + rc, err := ValidateConfig(t.Context(), "", config, false, options) assert.ErrorContains(t, err, "output image format must be specified") // Pass the output image format only in the config. config.Output.Image.Format = outputImageFormatAsConfig // The output image file should be set to the value in the config. - rc, err = ValidateConfig(t.Context(), configPath, config, false, options) + rc, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.OutputImageFormat, outputImageFormatAsConfig) @@ -984,7 +992,7 @@ func TestValidateConfig_OutputImageFormatSelection(t *testing.T) { // The output image file should be set to the value passed as an // argument. - rc, err = ValidateConfig(t.Context(), configPath, config, false, options) + rc, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.OutputImageFormat, outputImageFormatAsArg) @@ -993,7 +1001,7 @@ func TestValidateConfig_OutputImageFormatSelection(t *testing.T) { // The output image file should be set to the value passed as an // argument. - rc, err = ValidateConfig(t.Context(), configPath, config, false, options) + rc, err = ValidateConfig(t.Context(), "", config, false, options) assert.NoError(t, err) assert.Equal(t, rc.OutputImageFormat, outputImageFormatAsArg) } diff --git a/toolkit/tools/pkg/imagecustomizerlib/resolvedconfig.go b/toolkit/tools/pkg/imagecustomizerlib/resolvedconfig.go index c1cb3eaa39..e119a1b8c6 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/resolvedconfig.go +++ b/toolkit/tools/pkg/imagecustomizerlib/resolvedconfig.go @@ -14,7 +14,7 @@ import ( // ResolvedConfig contains the final image configuration, including the merged CLI and config values. type ResolvedConfig struct { // Configurations - BaseConfigPath string + ConfigFilePath string Config *imagecustomizerapi.Config Options ImageCustomizerOptions CustomizeOSPartitions bool @@ -48,6 +48,9 @@ type ResolvedConfig struct { // SELinux SELinux imagecustomizerapi.SELinux + // Storage + Storage imagecustomizerapi.Storage + // Bootloader BootLoader imagecustomizerapi.BootLoader @@ -58,7 +61,12 @@ type ResolvedConfig struct { Uki *imagecustomizerapi.Uki // Hierarchical config chain - ConfigChain []*ConfigWithBasePath + ConfigChain []*ConfigWithPath +} + +// ConfigDir returns the directory containing the config file +func (c *ResolvedConfig) ConfigDir() string { + return filepath.Dir(c.ConfigFilePath) } func (c *ResolvedConfig) InputFileExt() string { diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-base.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-base.yaml index 713dbbc423..9d0a1ecd11 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-base.yaml +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-base.yaml @@ -15,7 +15,43 @@ output: - ukis path: ./artifacts-1 +storage: + bootType: efi + disks: + - partitionTableType: gpt + partitions: + - id: esp + type: esp + size: 8M + + - id: usr + size: 2G + + - id: usrhash + size: 100M + + filesystems: + - deviceId: esp + type: fat32 + mountPoint: + path: /boot/efi + options: umask=0077 + + - deviceId: verityusr + type: ext4 + mountPoint: + path: /usr + options: ro + + verity: + - id: verityusr + name: usr + dataDeviceId: usr + hashDeviceId: usrhash + os: + bootloader: + resetType: hard-reset hostname: test-hostname-base users: - name: test-user-base diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-storage-reinitverity.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-storage-reinitverity.yaml new file mode 100644 index 0000000000..c30ff08686 --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-storage-reinitverity.yaml @@ -0,0 +1,10 @@ +previewFeatures: +- base-configs +- reinitialize-verity + +storage: + reinitializeVerity: all + +baseConfigs: + - path: hierarchical-config.yaml + diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-storage-resetuuid.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-storage-resetuuid.yaml new file mode 100644 index 0000000000..acda0e6b55 --- /dev/null +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config-storage-resetuuid.yaml @@ -0,0 +1,12 @@ +previewFeatures: +- base-configs + +storage: + resetPartitionsUuidsType: reset-all + +baseConfigs: + - path: hierarchical-config.yaml + +os: + bootloader: + resetType: hard-reset diff --git a/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config.yaml b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config.yaml index 4040e7cb23..82f3bbc724 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config.yaml +++ b/toolkit/tools/pkg/imagecustomizerlib/testdata/hierarchical-config.yaml @@ -3,15 +3,22 @@ storage: - partitionTableType: gpt partitions: - id: esp + label: esp type: esp size: 100M - id: boot + label: boot size: 100M - - id: rootfs + - id: root + label: root size: 2G + - id: roothash + label: roothash + size: 100M + bootType: efi filesystems: @@ -26,10 +33,18 @@ storage: mountPoint: path: /boot - - deviceId: rootfs + - deviceId: verityroot type: ext4 mountPoint: path: / + options: ro + + verity: + - id: verityroot + name: root + dataDeviceId: root + hashDeviceId: roothash + corruptionOption: panic previewFeatures: - output-artifacts