Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/user/explanation/config-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ azldev validates each config file against its schema as it is loaded. Unknown fi
azldev config generate-schema
```

Use `azldev config dump -q -O json` to inspect the fully resolved configuration after all includes are merged and inheritance is applied.
Use `azldev config dump -q -f json` to inspect the fully resolved configuration after all includes are merged, component inheritance is applied, and per-file overlays are expanded. The dump retains the default configuration blocks alongside the resolved component entries, so it is an inspection snapshot and must not be reloaded as project configuration.

## Related Resources

Expand Down
4 changes: 2 additions & 2 deletions docs/user/explanation/repos.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Why split RPM-build vs image-build? They have different security envelopes:
Expansion is deterministic and happens once during `ProjectConfig.Validate()`, after **all** config files (project, user, `--config-file` extras) have been merged. This means:

- a later config file can override an earlier set's `base-uri` or `gpg-key`;
- the merged config in `azldev config dump` is round-trippable: what you see is what you authored, and re-loading the dumped TOML produces the same effective state;
- `azldev config dump` emits an inspection snapshot of the effective configuration; it is not intended to be reloaded as project configuration;
- consumers (mock, kiwi) call `ResourcesConfig.EffectiveRpmRepos()` to get the flat resolved map without re-parsing layouts.

`gpg-key` paths follow the usual rule: bare paths are resolved relative to the directory of the file that defines them, then re-emitted as a `file` URI. URI-shaped values (an `http` or `https` URI, or a `file` URI with an absolute path) are passed through unchanged.
Expand Down Expand Up @@ -192,7 +192,7 @@ image-build = [
Inspect the fully resolved configuration with:

```sh
azldev config dump -q -O json | jq '.resources, .distros.mydistro.versions["4.0"].inputs'
azldev config dump -q -f json | jq '.resources, .distros.mydistro.versions["4.0"].inputs'
```

## Authoring tips
Expand Down
8 changes: 5 additions & 3 deletions docs/user/reference/cli/azldev_config_dump.md

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

2 changes: 1 addition & 1 deletion docs/user/reference/config/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,5 @@ includes = ["**/*.comp.toml"]

- [Configuration System](../../explanation/config-system.md) — how config files are loaded, merged, and how inheritance works
- [JSON Schema](../../../../schemas/azldev.schema.json) — machine-readable schema for editor integration and validation
- Run `azldev config dump -q -O json` to inspect the fully resolved configuration
- Run `azldev config dump -q -f json` to inspect the fully resolved configuration. The output is an inspection snapshot, not project configuration intended to be loaded again.
- Run `azldev config generate-schema` to generate the latest schema
34 changes: 30 additions & 4 deletions internal/app/azldev/cmds/config/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"slices"

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/components"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/pelletier/go-toml/v2"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -63,9 +64,11 @@ func newDumpCmd() *cobra.Command {
Long: `Dump the fully resolved project configuration.

Shows the merged result of all config files (embedded defaults, project
config, includes, and any extra --config-file arguments) after inheritance
and merge rules have been applied. Useful for debugging configuration
issues or inspecting effective values.`,
config, includes, and any extra --config-file arguments). Component entries
include inherited defaults and overlays expanded from 'overlay-files'.

The output is an inspection snapshot for debugging and scripting, not project
configuration intended to be loaded again.`,
Example: ` # Dump config as TOML (default)
azldev config dump

Expand Down Expand Up @@ -100,7 +103,10 @@ issues or inspecting effective values.`,
}

func DumpConfig(env *azldev.Env, format configDumpFormat) (string, error) {
config := portableConfigCopy(env.Config())
config, err := resolvedConfigCopy(env)
if err != nil {
return "", err
}

switch format {
case ConfigDumpFormatTOML:
Expand All @@ -122,6 +128,26 @@ func DumpConfig(env *azldev.Env, format configDumpFormat) (string, error) {
}
}

func resolvedConfigCopy(env *azldev.Env) (*projectconfig.ProjectConfig, error) {
resolved, err := components.NewResolver(env).FindAllComponents()
if err != nil {
return nil, fmt.Errorf("failed to resolve components:\n%w", err)
}

config := *env.Config()

config.Components = make(map[string]projectconfig.ComponentConfig, resolved.Len())
for _, component := range resolved.Components() {
componentConfig := *component.GetConfig()
// Drop resolver-populated runtime state so it doesn't leak into the JSON dump.
componentConfig.Locked = nil
componentConfig.RenderedSpecDir = ""
config.Components[component.GetName()] = componentConfig
}

return portableConfigCopy(&config), nil
}

func portableConfigCopy(config *projectconfig.ProjectConfig) *projectconfig.ProjectConfig {
result := *config

Expand Down
154 changes: 111 additions & 43 deletions internal/app/azldev/cmds/config/dump_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,136 @@
package config_test

import (
"context"
"encoding/json"
"testing"

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/cmds/config"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/testutils"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileperms"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDumpConfig(t *testing.T) {
const (
testProjectRoot = "/non/existent/dir"
testLogDir = "/non/existent/logs"
testWorkDir = "/non/existent/work"
testOutputDir = "/non/existent/output"
)
testEnv := testutils.NewTestEnv(t)
testEnv.Config.Project.RenderedSpecsDir = "/project/specs"
testEnv.Config.DefaultComponentConfig.Build.With = []string{"project-default"}
testEnv.Config.Components["example"] = projectconfig.ComponentConfig{
Name: "example",
OverlayFiles: []string{"/project/components/example.overlay.toml"},
SourceFiles: []projectconfig.SourceFileReference{{
Filename: "generated.tar.gz",
Origin: projectconfig.Origin{
Type: projectconfig.OriginTypeCustom,
Script: "/project/components/generate.sh",
},
}},
}
testEnv.Config.ComponentGroups["example-group"] = projectconfig.ComponentGroupConfig{
Components: []string{"example"},
DefaultComponentConfig: projectconfig.ComponentConfig{
Build: projectconfig.ComponentBuildConfig{With: []string{"group-default"}},
},
}
testEnv.Config.GroupsByComponent["example"] = []string{"example-group"}
distro := testEnv.Config.Distros["test-distro"]
distroVersion := distro.Versions["1.0"]
distroVersion.DefaultComponentConfig.Build.With = []string{"distro-default"}
distro.Versions["1.0"] = distroVersion
testEnv.Config.Distros["test-distro"] = distro
testEnv.WriteDefaultLock(t, "example")
require.NoError(t, fileutils.WriteFile(
testEnv.TestFS,
"/project/components/example.overlay.toml",
[]byte(`[metadata]
category = "azl-branding-policy"
upstream-status = "inapplicable"

[[overlays]]
type = "spec-set-tag"
tag = "Vendor"
value = "Microsoft"
`),
fileperms.PrivateFile,
))

cfg := &projectconfig.ProjectConfig{
Project: projectconfig.ProjectInfo{
LogDir: testLogDir,
WorkDir: testWorkDir,
OutputDir: testOutputDir,
testCases := []struct {
name string
dump func() (string, error)
unmarshal func([]byte, any) error
}{
{
name: "TOML",
dump: func() (string, error) {
return config.DumpConfig(testEnv.Env, config.ConfigDumpFormatTOML)
},
unmarshal: toml.Unmarshal,
},
Components: map[string]projectconfig.ComponentConfig{
"example": {
SourceFiles: []projectconfig.SourceFileReference{{
Filename: "generated.tar.gz",
Origin: projectconfig.Origin{
Type: projectconfig.OriginTypeCustom,
Script: "/project/components/generate.sh",
},
}},
{
name: "JSON",
dump: func() (string, error) {
return config.DumpConfig(testEnv.Env, config.ConfigDumpFormatJSON)
},
unmarshal: json.Unmarshal,
},
}

ctx, cancelFunc := context.WithCancel(t.Context())
defer cancelFunc()
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
configText, err := testCase.dump()
require.NoError(t, err)

testEnv := testutils.NewTestEnv(t)
var dumped projectconfig.ProjectConfig
require.NoError(t, testCase.unmarshal([]byte(configText), &dumped))

options := azldev.NewEnvOptions()
options.DryRunnable = testEnv.DryRunnable
options.EventListener = testEnv.EventListener
options.Interfaces = testEnv.TestInterfaces
options.ProjectDir = testProjectRoot
options.Config = cfg
dumpedComponent := dumped.Components["example"]
assert.Equal(t,
[]string{"distro-default", "project-default", "group-default"},
dumpedComponent.Build.With,
)
require.Len(t, dumpedComponent.Overlays, 1)
assert.Equal(t, "Vendor", dumpedComponent.Overlays[0].Tag)
assert.Empty(t, dumpedComponent.OverlayFiles)
assert.Nil(t, dumpedComponent.Locked)
assert.Empty(t, dumpedComponent.RenderedSpecDir)
assert.Equal(t, []string{"project-default"}, dumped.DefaultComponentConfig.Build.With)
assert.Equal(t, []string{"group-default"},
dumped.ComponentGroups["example-group"].DefaultComponentConfig.Build.With)
assert.Equal(t, "generate.sh", dumpedComponent.SourceFiles[0].Origin.Script)
})
}

env := azldev.NewEnv(ctx, options)
originalComponent := testEnv.Config.Components["example"]
assert.Equal(t, []string{"/project/components/example.overlay.toml"}, originalComponent.OverlayFiles)
assert.Empty(t, originalComponent.Overlays)
assert.Nil(t, originalComponent.Locked)
assert.Empty(t, originalComponent.RenderedSpecDir)
assert.Equal(t, "/project/components/generate.sh", originalComponent.SourceFiles[0].Origin.Script)
}

configText, err := config.DumpConfig(env, config.ConfigDumpFormatTOML)
require.NoError(t, err)
require.NotEmpty(t, configText)
require.Contains(t, configText, "generate.sh")
require.NotContains(t, configText, "/project/components")
func TestDumpConfigIncludesSpecDiscoveredComponents(t *testing.T) {
testEnv := testutils.NewTestEnv(t)
testEnv.Config.ComponentGroups["local-specs"] = projectconfig.ComponentGroupConfig{
SpecPathPatterns: []string{"/project/specs/**/*.spec"},
}
require.NoError(t, fileutils.WriteFile(
testEnv.TestFS,
"/project/specs/example/example.spec",
[]byte("Name: example\n"),
fileperms.PrivateFile,
))

configText, err = config.DumpConfig(env, config.ConfigDumpFormatJSON)
configText, err := config.DumpConfig(testEnv.Env, config.ConfigDumpFormatJSON)
require.NoError(t, err)
require.NotEmpty(t, configText)
require.Contains(t, configText, "generate.sh")
require.NotContains(t, configText, "/project/components")
require.Equal(t, "/project/components/generate.sh",
cfg.Components["example"].SourceFiles[0].Origin.Script)

var dumped projectconfig.ProjectConfig
require.NoError(t, json.Unmarshal([]byte(configText), &dumped))

dumpedComponent, found := dumped.Components["example"]
require.True(t, found)
assert.Equal(t, projectconfig.SpecSourceTypeLocal, dumpedComponent.Spec.SourceType)
assert.Equal(t, "/project/specs/example/example.spec", dumpedComponent.Spec.Path)
}
Loading