Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0eb8f89
initial
elainezhao1 Sep 30, 2025
17d28a8
updates
elainezhao1 Oct 1, 2025
4c2f544
updates
elainezhao1 Oct 2, 2025
a6e3ccd
address some comments
elainezhao1 Oct 3, 2025
e90f0a7
rebase
elainezhao1 Oct 3, 2025
a80847b
rebase
elainezhao1 Oct 3, 2025
2e8bbdd
update schema
elainezhao1 Oct 3, 2025
2ba2a8f
address more feedback
elainezhao1 Oct 7, 2025
3e0aad7
Merge branch 'main' into user/elaine/poc1
elainezhao1 Oct 7, 2025
e10c7d1
update tests
elainezhao1 Oct 7, 2025
15e0bd6
Merge ValidateConfig and createImageCustomizerParameters.
cwize1 Oct 7, 2025
7a61336
Bug fix
cwize1 Oct 7, 2025
3017e68
Feedback updates
cwize1 Oct 8, 2025
ed28023
Split off ResolvedConfig from ImageCustomizerParameters.
cwize1 Oct 10, 2025
efc0bf2
resolve and rebase
elainezhao1 Oct 13, 2025
deb63e5
another rebase
elainezhao1 Oct 13, 2025
6ea0f26
Currently, the `ValidateConfig` and the `createResolvedConfig`
cwize1 Oct 13, 2025
89eaa04
another rebase
elainezhao1 Oct 14, 2025
4070cdb
another rebase
elainezhao1 Oct 15, 2025
9737ab1
another rebase
elainezhao1 Oct 15, 2025
f9dc60b
address comments
elainezhao1 Oct 15, 2025
a46a4b6
address more comments
elainezhao1 Oct 16, 2025
b1a4166
resolve comments
elainezhao1 Oct 17, 2025
7d8022e
add doc and rename test files
elainezhao1 Oct 17, 2025
344f754
add doc and rename test files
elainezhao1 Oct 17, 2025
b8895b7
update doc
elainezhao1 Oct 17, 2025
a6ea660
line wrap
elainezhao1 Oct 17, 2025
fcbee70
Merge branch 'main' into user/elaine/poc1
elainezhao1 Oct 17, 2025
2ba46bf
resolve more comments
elainezhao1 Oct 20, 2025
2bff675
Merge branch 'user/elaine/poc1' of github.com:microsoft/azure-linux-i…
elainezhao1 Oct 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/imagecustomizer/api/configuration/baseConfig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
parent: Configuration
ancestor: Image Customizer
---

# baseConfig type

This is a preview feature.
Its API and behavior is subject to change.
You must enable this feature by specifying `base-configs` in the
[previewFeatures](./config.md#previewfeatures-string) API.

Defines a single configuration file to inherit from.

BaseConfigs specifies a list of base configuration files to inherit from.
When multiple base configs are specified, fields are resolved in order —
fields in later configurations overriding or extending earlier ones.

**The current(last) config’s value (if specified) overrides all base configs.**

- `.input.image.path`
- `.output.image.path`
- `.output.image.format`
- `.output.artifacts.path`

**Base config items are merged with current config’s items**

- `.output.artifacts.items`

## path [string]

Required.

A file path to the base config file. The path can be either relative or absolute.
Relative paths are resolved relative to the parent directory of the current config file.

Example:

```yaml
baseConfigs:
- path: ./base-config.yaml
- path: /absolute/path/to/base-config.yaml
```

Added in v1.1.0.
9 changes: 9 additions & 0 deletions docs/imagecustomizer/api/configuration/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ Supported options:

Added in v0.15.

- `base-configs`: Enables support for hierarchical configuration inheritance.

When this option is specified, the `baseConfigs` configuration becomes available.
This allows configurations to inherit from one or more base configurations.

See [Base Config](./baseConfig.md) for more details.

Added in v1.1.0.

## output [[output](./output.md)]
Copy link
Contributor

Choose a reason for hiding this comment

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

You need to add the baseConfigs field to this file.


Specifies the configuration for the output image and artifacts.
Expand Down
20 changes: 20 additions & 0 deletions toolkit/tools/imagecustomizerapi/baseconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package imagecustomizerapi

import (
"fmt"
"strings"
)

type BaseConfig struct {
Path string `yaml:"path" json:"path"`
}

func (b BaseConfig) IsValid() error {
if strings.TrimSpace(b.Path) == "" {
return fmt.Errorf("path must not be empty or whitespace")
}
return nil
}
28 changes: 28 additions & 0 deletions toolkit/tools/imagecustomizerapi/baseconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package imagecustomizerapi

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestBaseConfigIsValidNoPath(t *testing.T) {
base := BaseConfig{
Path: "",
}
err := base.IsValid()
assert.Error(t, err)
assert.ErrorContains(t, err, "path must not be empty or whitespace")
}

func TestBaseConfigIsValidWhitespaces(t *testing.T) {
base := BaseConfig{
Path: " ",
}
err := base.IsValid()
assert.Error(t, err)
assert.ErrorContains(t, err, "path must not be empty or whitespace")
}
13 changes: 13 additions & 0 deletions toolkit/tools/imagecustomizerapi/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Config struct {
Scripts Scripts `yaml:"scripts" json:"scripts,omitempty"`
PreviewFeatures []PreviewFeature `yaml:"previewFeatures" json:"previewFeatures,omitempty"`
Output Output `yaml:"output" json:"output,omitempty"`
BaseConfigs []BaseConfig `yaml:"baseConfigs" json:"baseConfigs,omitempty"`
}

func (c *Config) IsValid() (err error) {
Expand Down Expand Up @@ -123,6 +124,18 @@ func (c *Config) IsValid() (err error) {
return fmt.Errorf("the 'reinitialize-verity' preview feature must be enabled to use 'storage.reinitializeVerity'")
}

if c.BaseConfigs != nil {
if !sliceutils.ContainsValue(c.PreviewFeatures, PreviewFeatureBaseConfigs) {
return fmt.Errorf("the '%s' preview feature must be enabled to use 'baseConfigs'", PreviewFeatureBaseConfigs)
}

for i, base := range c.BaseConfigs {
if err := base.IsValid(); err != nil {
return fmt.Errorf("invalid baseConfig item at index %d:\n%w", i, err)
}
}
}

return nil
}

Expand Down
5 changes: 4 additions & 1 deletion toolkit/tools/imagecustomizerapi/previewfeaturetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ const (

// PreviewFeatureFedora42 enables support for Fedora 42 images.
PreviewFeatureFedora42 PreviewFeature = "fedora-42"

// PreviewFeatureBaseConfigs enables support for base configuration.
PreviewFeatureBaseConfigs PreviewFeature = "base-configs"
)

func (pf PreviewFeature) IsValid() error {
switch pf {
case PreviewFeatureUki, PreviewFeatureOutputArtifacts, PreviewFeatureInjectFiles, PreviewFeaturePackageSnapshotTime,
PreviewFeatureKdumpBootFiles, PreviewFeatureFedora42:
PreviewFeatureKdumpBootFiles, PreviewFeatureFedora42, PreviewFeatureBaseConfigs:
return nil
default:
return fmt.Errorf("invalid preview feature: %s", pf)
Expand Down
15 changes: 15 additions & 0 deletions toolkit/tools/imagecustomizerapi/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@
"additionalProperties": false,
"type": "object"
},
"BaseConfig": {
"properties": {
"path": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object"
},
"BootLoader": {
"properties": {
"resetType": {
Expand Down Expand Up @@ -82,6 +91,12 @@
},
"output": {
"$ref": "#/$defs/Output"
},
"baseConfigs": {
"items": {
"$ref": "#/$defs/BaseConfig"
},
"type": "array"
}
},
"additionalProperties": false,
Expand Down
70 changes: 70 additions & 0 deletions toolkit/tools/pkg/imagecustomizerlib/baseconfigs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package imagecustomizerlib

import (
"context"
"fmt"

"github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi"
"github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/file"
)

type ConfigWithBasePath struct {
Config *imagecustomizerapi.Config
BaseConfigPath string
}

func buildConfigChain(ctx context.Context, rc *ResolvedConfig) ([]*ConfigWithBasePath, error) {
visited := make(map[string]bool)
pathStack := []string{}

configChain, err := buildConfigChainHelper(ctx, rc.Config, rc.BaseConfigPath, visited, pathStack)
if err != nil {
return nil, err
}

return configChain, nil
}

func buildConfigChainHelper(ctx context.Context, cfg *imagecustomizerapi.Config, configFilePath string, visited map[string]bool,
pathStack []string,
) ([]*ConfigWithBasePath, error) {
var chain []*ConfigWithBasePath

for _, base := range cfg.BaseConfigs {
absPath := file.GetAbsPathWithBase(configFilePath, base.Path)

if visited[absPath] {
return nil, fmt.Errorf("cycle detected in baseConfigs: %v -> %s", pathStack, absPath)
}

visited[absPath] = true
pathStack = append(pathStack, absPath)

// Load base file into struct
var baseCfg imagecustomizerapi.Config
if err := imagecustomizerapi.UnmarshalYamlFile(absPath, &baseCfg); err != nil {
return nil, fmt.Errorf("failed to load base config (%s):\n%w", absPath, err)
}

// Validate base config content
if err := baseCfg.IsValid(); err != nil {
return nil, fmt.Errorf("%w (%s):\n%w", ErrInvalidBaseConfigs, absPath, err)
}

// Recurse into base config
subChain, err := buildConfigChainHelper(ctx, &baseCfg, absPath, visited, pathStack)
if err != nil {
return nil, err
}

chain = append(chain, subChain...)
}

// Add the current config last
chain = append(chain, &ConfigWithBasePath{
Config: cfg,
BaseConfigPath: configFilePath,
})

return chain, nil
}
51 changes: 51 additions & 0 deletions toolkit/tools/pkg/imagecustomizerlib/baseconfigs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package imagecustomizerlib

import (
"os"
"path/filepath"
"testing"

"github.com/microsoft/azure-linux-image-tools/toolkit/tools/imagecustomizerapi"
"github.com/microsoft/azure-linux-image-tools/toolkit/tools/internal/file"
"github.com/stretchr/testify/assert"
)

func TestBaseConfigsInputAndOutput(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good to have at least one test that actually runs through an actual image customization.

Copy link
Contributor Author

@elainezhao1 elainezhao1 Oct 18, 2025

Choose a reason for hiding this comment

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

to add a full run functional test, any suggestions on how I can obtain a test image from which I can extract 2 artifacts (to test merged artifact items) ? I think current empty.vhdx would not work. if it is not ok to check in a non empty vhdx, can I exclude the artifacts item in the full run?

Copy link
Contributor

Choose a reason for hiding this comment

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

See the checkSkipForCustomizeDefaultImage function. It is used quite extensively in the functional tests.

You can pass in the base image to the test using the --base-image-core-efi-azl3 CLI option when running go test.

testTempDir := filepath.Join(tmpDir, "TestBaseConfigsInputAndOutput")
defer os.RemoveAll(testTempDir)

buildDir := filepath.Join(testTempDir, "build")
currentConfigFile := filepath.Join(testDir, "hierarchical-config.yaml")

options := ImageCustomizerOptions{
BuildDir: buildDir,
}

var config imagecustomizerapi.Config
err := imagecustomizerapi.UnmarshalYamlFile(currentConfigFile, &config)
assert.NoError(t, err)

rc, err := ValidateConfig(t.Context(), testDir, &config, false, options)
assert.NoError(t, err)

// Verify resolved values
expectedInputPath := file.GetAbsPathWithBase(testDir, "testimages/empty.vhdx")
expectedOutputPath := file.GetAbsPathWithBase(testDir, "./out/output-image-2.vhdx")
expectedArtifactsPath := file.GetAbsPathWithBase(testDir, "./artifacts-2")

assert.Equal(t, expectedInputPath, rc.InputImageFile)
assert.Equal(t, expectedOutputPath, rc.OutputImageFile)
assert.Equal(t, expectedArtifactsPath, rc.OutputArtifacts.Path)
assert.Equal(t, "testname", rc.Config.OS.Hostname)

// Verify merged artifact items
expectedItems := []imagecustomizerapi.OutputArtifactsItemType{
imagecustomizerapi.OutputArtifactsItemUkis,
imagecustomizerapi.OutputArtifactsItemShim,
}
actual := rc.OutputArtifacts.Items
assert.Equal(t, len(expectedItems), len(actual))

assert.ElementsMatch(t, expectedItems, actual,
"output artifact items should match - expected: %v, got: %v", expectedItems, actual)
}
Loading
Loading