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
12 changes: 12 additions & 0 deletions docs/user/reference/config/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ The `[images]` section defines system images (VMs, containers, etc.) that azldev
| Capabilities | `capabilities` | [ImageCapabilities](#image-capabilities) | No | Describes features and properties of this image |
| Tests | `tests` | [ImageTests](#image-tests) | No | Test configuration for this image |
| Publish | `publish` | [ImagePublish](#image-publish) | No | Publishing settings for this image |
| Architectures | `architectures` | string array | No | Architectures supported by this image |

The current supported architectures are `x86_64` and `aarch64`. `architectures` is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question(non-blocking): Is this the appropriate spot for this description of Architectures? I definitely like having it but I don't see anything else doing being described here. I wonder if there's a better place for it, but this is definitely non-blocking.

optional; omitting it (or leaving it empty) means the image is unrestricted and
supports all recognized architectures, which keeps images.toml files written before
this field existed valid. `azldev image list` reports each image's architecture set,
and `azldev image build --arch` rejects architectures outside a declared set.

## Image Definition

Expand Down Expand Up @@ -64,6 +71,7 @@ The `publish` subtable configures where an image is published. Unlike packages (
[images.vm-base]
description = "VM Base Image"
definition = { type = "kiwi", path = "vm-base/vm-base.kiwi" }
architectures = ["x86_64", "aarch64"]

[images.vm-base.capabilities]
machine-bootable = true
Expand All @@ -77,6 +85,7 @@ runtime-package-management = true
[images.container-base]
description = "Container Base Image"
definition = { type = "kiwi", path = "container-base/container-base.kiwi" }
architectures = ["x86_64", "aarch64"]

[images.container-base.capabilities]
container = true
Expand All @@ -88,6 +97,7 @@ container = true
[images.vm-azure]
description = "Azure-optimized VM image"
definition = { type = "kiwi", path = "vm-azure/vm-azure.kiwi", profile = "azure" }
architectures = ["x86_64"]
```

### Image with test suite references
Expand All @@ -96,6 +106,7 @@ definition = { type = "kiwi", path = "vm-azure/vm-azure.kiwi", profile = "azure"
[images.vm-base]
description = "VM Base Image"
definition = { type = "kiwi", path = "vm-base/vm-base.kiwi" }
architectures = ["x86_64", "aarch64"]

[images.vm-base.capabilities]
machine-bootable = true
Expand All @@ -114,6 +125,7 @@ test-suites = [
[images.vm-base]
description = "VM Base Image"
definition = { type = "kiwi", path = "vm-base/vm-base.kiwi" }
architectures = ["x86_64", "aarch64"]

[images.vm-base.publish]
channels = ["registry-prod", "registry-staging"]
Expand Down
8 changes: 8 additions & 0 deletions internal/app/azldev/agentskill/agentskill_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ func TestImageSkillDocumentsRuntimeConfigOverride(t *testing.T) {
assert.Contains(t, doc, "`kiwi-config-override`")
}

func TestImageSkillDocumentsArchitecturesField(t *testing.T) {
doc, err := agentskill.SkillDocument("azldev-image", testParams())
require.NoError(t, err)

assert.Contains(t, doc, "architectures = ")
assert.Contains(t, doc, "unrestricted")
}

func TestSkillFrontmatterInvariants(t *testing.T) {
layout := agentskill.DefaultLayout()

Expand Down
4 changes: 4 additions & 0 deletions internal/app/azldev/agentskill/content/image.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Images are declared under `[images.<name>]` (conventionally in an `images.toml`)
[images.container-base]
description = "Container base image"
definition = { type = "kiwi", path = "container-base/container-base.kiwi", profile = "core" }
architectures = ["x86_64", "aarch64"]

[images.container-base.capabilities]
container = true
Expand All @@ -50,6 +51,9 @@ definition = { type = "kiwi", path = "container-base/container-base.kiwi", profi
`profile` selects a kiwi profile (optional).
- `capabilities` are tri-state flags describing the image — `machine-bootable`,
`container`, `systemd`, `runtime-package-management`. Set only the ones that apply.
- `architectures = ["x86_64", "aarch64"]` is optional; when unset or empty, the
image is treated as unrestricted (all recognized architectures). Set it to
restrict which architectures `image build --arch` allows for the image.
- `tests.test-suites` lists the test suites `azldev image test` runs.
- `publish.channels` lists the channels the image publishes to.

Expand Down
60 changes: 58 additions & 2 deletions internal/app/azldev/cmds/image/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"fmt"
"log/slog"
"path/filepath"
"runtime"
"slices"

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/workdir"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/kiwi"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/qemu"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -150,8 +153,7 @@ func BuildImage(env *azldev.Env, options *ImageBuildOptions) (*ImageBuildResult,
return nil, err
}

// Resolve the image from config.
imageConfig, err := ResolveImageByName(env, options.ImageName)
imageConfig, err := resolveBuildImage(env, options)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -232,6 +234,60 @@ func BuildImage(env *azldev.Env, options *ImageBuildOptions) (*ImageBuildResult,
}, nil
}

func resolveBuildImage(env *azldev.Env, options *ImageBuildOptions) (*projectconfig.ImageConfig, error) {
imageConfig, err := ResolveImageByName(env, options.ImageName)
if err != nil {
return nil, err
}

if err := validateBuildArchitecture(
imageConfig,
options.TargetArch,
runtime.GOARCH,
); err != nil {
return nil, err
}

return imageConfig, nil
}

func validateBuildArchitecture(
imageConfig *projectconfig.ImageConfig,
targetArch ImageArch,
hostGoArch string,
) error {
arch := string(targetArch)
if arch == "" {
arch = qemu.GoArchToQEMUArch(hostGoArch)
if !slices.Contains(qemu.SupportedArchitectures(), arch) {
return fmt.Errorf("unsupported host architecture %#q", hostGoArch)
}
}

// SupportsArchitecture rejects both architectures the image doesn't declare
// support for and architectures azldev doesn't recognize at all (relevant for
// an unrestricted image with no declared Architectures, and for an explicit
// --arch value that bypassed ImageArch.Set's validation).
if !imageConfig.SupportsArchitecture(arch) {
supportedArchitectures := imageConfig.Architectures
if len(supportedArchitectures) == 0 {
// An unrestricted image supports every recognized architecture; report
// that set instead of the empty declared list, which would otherwise
// misleadingly suggest the image supports none.
supportedArchitectures = projectconfig.SupportedImageArchitectures()
}

return fmt.Errorf(
"image %#q does not support architecture %#q; supported architectures: %q",
imageConfig.Name,
arch,
supportedArchitectures,
)
}

return nil
}

// checkBuildPrerequisites verifies that required tools are available for building images.
func checkBuildPrerequisites(env *azldev.Env) error {
if err := kiwi.CheckPrerequisites(env); err != nil {
Expand Down
51 changes: 51 additions & 0 deletions internal/app/azldev/cmds/image/build_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,54 @@ func TestCreateKiwiRunnerDistroConfigOverride(t *testing.T) {
})
}
}

func TestValidateBuildArchitecture(t *testing.T) {
imageConfig := &projectconfig.ImageConfig{
Name: "gen1",
Architectures: []string{projectconfig.ImageArchitectureX86_64},
}

require.NoError(t, validateBuildArchitecture(
imageConfig,
ImageArchX86_64,
"arm64",
))
require.NoError(t, validateBuildArchitecture(
imageConfig,
ImageArchDefault,
"amd64",
))

err := validateBuildArchitecture(imageConfig, ImageArchAarch64, "amd64")
require.ErrorContains(t, err, "image `gen1` does not support architecture `aarch64`")

err = validateBuildArchitecture(imageConfig, ImageArchDefault, "arm64")
require.ErrorContains(t, err, "image `gen1` does not support architecture `aarch64`")

err = validateBuildArchitecture(imageConfig, ImageArchDefault, "riscv64")
require.ErrorContains(t, err, "unsupported host architecture `riscv64`")
}

func TestValidateBuildArchitecture_UnrestrictedWhenUnset(t *testing.T) {
// An image with no declared Architectures (e.g. from an images.toml written
// before this field existed) must remain unrestricted.
imageConfig := &projectconfig.ImageConfig{Name: "legacy"}

require.NoError(t, validateBuildArchitecture(imageConfig, ImageArchX86_64, "amd64"))
require.NoError(t, validateBuildArchitecture(imageConfig, ImageArchAarch64, "amd64"))
}

func TestValidateBuildArchitecture_RejectsUnsupportedExplicitTarget(t *testing.T) {
// An unrestricted image (no declared Architectures) must still reject an
// explicit --arch value that bypasses ImageArch.Set (e.g. set directly rather
// than via flag parsing), instead of silently accepting any string.
// SupportsArchitecture rejects it because "riscv64" isn't a recognized
// architecture at all, regardless of the image's declared support. The error
// must report the recognized architecture set, not the image's empty
// declared list (which would misleadingly suggest it supports none).
imageConfig := &projectconfig.ImageConfig{Name: "legacy"}

err := validateBuildArchitecture(imageConfig, ImageArch("riscv64"), "amd64")
require.ErrorContains(t, err, "image `legacy` does not support architecture `riscv64`")
require.ErrorContains(t, err, `supported architectures: ["x86_64" "aarch64"]`)
}
21 changes: 17 additions & 4 deletions internal/app/azldev/cmds/image/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ type ImageListResult struct {
// display.
CapabilitiesSummary string `json:"-" table:"Capabilities"`

// Architectures lists the architectures supported by this image, as declared in
// its config. An empty list means the image is unrestricted (all recognized
// architectures), since the field is optional.
Architectures []string `json:"architectures" table:"-"`

// ArchitecturesSummary is a comma-separated summary for table display.
ArchitecturesSummary string `json:"-" table:"Architectures"`

// Tests holds the test configuration for this image, matching the original config
// structure.
Tests *projectconfig.ImageTestsConfig `json:"tests,omitempty" table:"-"`
Expand Down Expand Up @@ -135,10 +143,15 @@ func ListImages(env *azldev.Env, options *ListImageOptions) ([]ImageListResult,
Description: imageConfig.Description,
Capabilities: imageConfig.Capabilities,
CapabilitiesSummary: strings.Join(imageConfig.Capabilities.EnabledNames(), ", "),
Tests: imageConfig.Tests,
TestsSummary: strings.Join(imageConfig.TestNames(), ", "),
Publish: imageConfig.Publish,
PublishSummary: strings.Join(imageConfig.Publish.Channels, ", "),
Architectures: imageConfig.Architectures,
ArchitecturesSummary: strings.Join(
imageConfig.Architectures,
", ",
),
Tests: imageConfig.Tests,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit(non-blocking): I notice that you changed the indentation/justification for these fields. Was that intentional? If so, why?

TestsSummary: strings.Join(imageConfig.TestNames(), ", "),
Publish: imageConfig.Publish,
PublishSummary: strings.Join(imageConfig.Publish.Channels, ", "),
Definition: ImageDefinitionResult{
Type: string(imageConfig.Definition.DefinitionType),
Path: imageConfig.Definition.Path,
Expand Down
30 changes: 26 additions & 4 deletions internal/app/azldev/cmds/image/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,18 @@ func TestListImages_AllImages(t *testing.T) {
testEnv := testutils.NewTestEnv(t)
testEnv.Config.Images = map[string]projectconfig.ImageConfig{
"image-a": {
Name: "image-a",
Description: "Image A description",
Name: "image-a",
Description: "Image A description",
Architectures: []string{"x86_64", "aarch64"},
Definition: projectconfig.ImageDefinition{
DefinitionType: projectconfig.ImageDefinitionTypeKiwi,
Path: "/path/to/image-a.kiwi",
},
},
"image-b": {
Name: "image-b",
Description: "Image B description",
Name: "image-b",
Description: "Image B description",
Architectures: []string{"x86_64"},
Definition: projectconfig.ImageDefinition{
DefinitionType: projectconfig.ImageDefinitionTypeKiwi,
Path: "/path/to/image-b.kiwi",
Expand All @@ -72,11 +74,31 @@ func TestListImages_AllImages(t *testing.T) {
// Results should be sorted alphabetically by name.
assert.Equal(t, "image-a", results[0].Name)
assert.Equal(t, "Image A description", results[0].Description)
assert.Equal(t, []string{"x86_64", "aarch64"}, results[0].Architectures)
assert.Equal(t, "x86_64, aarch64", results[0].ArchitecturesSummary)
assert.Equal(t, "kiwi", results[0].Definition.Type)
assert.Equal(t, "/path/to/image-a.kiwi", results[0].Definition.Path)

assert.Equal(t, "image-b", results[1].Name)
assert.Equal(t, "Image B description", results[1].Description)
assert.Equal(t, []string{"x86_64"}, results[1].Architectures)
assert.Equal(t, "x86_64", results[1].ArchitecturesSummary)
}

func TestListImages_ArchitecturesPerImage(t *testing.T) {
testEnv := testutils.NewTestEnv(t)
testEnv.Config.Images = map[string]projectconfig.ImageConfig{
"gen1": {
Name: "gen1",
Architectures: []string{"x86_64"},
},
}

results, err := image.ListImages(testEnv.Env, &image.ListImageOptions{})
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, []string{"x86_64"}, results[0].Architectures)
assert.Equal(t, "x86_64", results[0].ArchitecturesSummary)
}

func TestListImages_WithCapabilitiesAndTests(t *testing.T) {
Expand Down
Loading
Loading