Skip to content
Draft
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
8 changes: 8 additions & 0 deletions internal/app/azldev/cmds/advanced/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,18 @@ func TestBuildRPMS(t *testing.T) {
Versions: map[string]projectconfig.DistroVersionDefinition{
"1.0": {
MockConfigPath: testMockConfigPath,
Inputs: projectconfig.DistroVersionInputs{
RpmBuild: []string{"test-repo"},
},
},
},
}

testEnv.Config.Resources.RpmRepos["test-repo"] = projectconfig.RpmRepoResource{
BaseURI: "https://example.com/test-repo/$basearch",
DisableGPGCheck: true,
}

// Pretend that "mock" exists.
testEnv.CmdFactory.RegisterCommandInSearchPath("mock")

Expand Down
12 changes: 10 additions & 2 deletions internal/app/azldev/cmds/component/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"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/app/azldev/core/mockconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources"
"github.com/microsoft/azure-linux-dev-tools/internal/global/opctx"
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"
Expand Down Expand Up @@ -1007,7 +1008,14 @@ func createMockProcessor(env *azldev.Env) *sources.MockProcessor {
return nil
}

slog.Info("Mock processor available", "mockConfig", distroVerDef.MockConfigPath)
preparedConfigPath, err := mockconfig.PrepareForRPMBuild(env)
if err != nil {
slog.Warn("Mock processor unavailable; failed to prepare mock config", "error", err)

return nil
}

slog.Info("Mock processor available", "mockConfig", preparedConfigPath)

return sources.NewMockProcessor(env, distroVerDef.MockConfigPath)
return sources.NewMockProcessor(env, preparedConfigPath)
}
114 changes: 113 additions & 1 deletion internal/app/azldev/cmds/image/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,12 @@ func createKiwiRunner(
runner.WithTargetArch(string(options.TargetArch))
}

// Build per-repo options for remote repositories.
// Inject TOML-defined image-build repos for the active distro version.
if err := addConfiguredImageBuildRepos(env, runner, options); err != nil {
return nil, err
}

// Build per-repo options for user-supplied remote repositories (additive).
remoteRepoOptions := &kiwi.RepoOptions{
DisableRepoGPGCheck: options.NoRemoteRepoGpgCheck,
ImageInclude: options.RemoteRepoIncludeInImage,
Expand All @@ -275,6 +280,113 @@ func createKiwiRunner(
return runner, nil
}

// addConfiguredImageBuildRepos resolves the active distro version's
// inputs.image-build list to RPM repo resources and registers each with the kiwi
// runner. When no inputs are configured for image-build the function is a no-op:
// images may still get their repos from the .kiwi description and/or user-supplied
// --repo flags. When inputs *are* configured but every entry is filtered out for
// the target architecture, that is treated as an error (it would otherwise
// silently produce a no-repo image).
func addConfiguredImageBuildRepos(
env *azldev.Env, runner *kiwi.Runner, options *ImageBuildOptions,
) error {
_, distroVerDef, err := env.Distro()
if err != nil {
return fmt.Errorf("failed to resolve distro for image build:\n%w", err)
}

repoNames := distroVerDef.Inputs.ImageBuild
if len(repoNames) == 0 {
// No TOML-driven image-build repos. The .kiwi description and/or
// user-supplied --repo flags are expected to supply repos.
slog.Info("No TOML-driven image-build inputs configured; relying on .kiwi/--repo for repositories")

return nil
}
Comment on lines +298 to +305

cfg := env.Config()
if cfg == nil {
return errors.New("no project config loaded; cannot resolve image-build inputs")
}

repos := cfg.Resources.RpmRepos

targetArch := string(options.TargetArch)

addedCount := 0
skippedForArch := []string{}

for _, name := range repoNames {
repo, ok := repos[name]
if !ok {
// Should have been caught by load-time validation, but defend defensively.
return fmt.Errorf("inputs.image-build references undefined rpm-repo %#q", name)
}

if targetArch != "" && !repo.IsAvailableForArch(targetArch) {
slog.Warn("Skipping rpm-repo for image-build (arch mismatch)",
"repo", name, "targetArch", targetArch, "repoArches", repo.Arches)

skippedForArch = append(skippedForArch, name)

continue
}

if err := addKiwiRepoFromResource(runner, name, &repo); err != nil {
return fmt.Errorf("failed to add image-build rpm-repo %#q:\n%w", name, err)
}

addedCount++
}

if addedCount == 0 {
return fmt.Errorf(
"all %d image-build rpm-repos were filtered out for target arch %q (skipped: %v); "+
"check the `arches` settings on these repos",
len(repoNames), targetArch, skippedForArch,
)
}

return nil
}

// addKiwiRepoFromResource registers a single [projectconfig.RpmRepoResource] with the
// kiwi runner via [kiwi.Runner.AddRemoteRepo], applying the repo's GPG-check / signing
// key and source-type semantics.
//
// `disable-gpg-check` is mapped to *both* kiwi GPG knobs (package and repo metadata).
// dnf treats `gpgcheck=0` as covering package signature verification, and we want the
// kiwi semantics to mirror that: a single TOML field means "don't enforce signatures
// at all for this repo". If we ever need finer control we can add a second field.
func addKiwiRepoFromResource(runner *kiwi.Runner, name string, repo *projectconfig.RpmRepoResource) error {
opts := &kiwi.RepoOptions{
Alias: name,
DisablePackageGPGCheck: repo.DisableGPGCheck,
DisableRepoGPGCheck: repo.DisableGPGCheck,
}

if repo.GPGKey != "" {
opts.SigningKeys = []string{repo.GPGKey}
}

source := repo.BaseURI
if source == "" && repo.Metalink != "" {
// kiwi v10.x supports metalink via repo_sourcetype.
source = repo.Metalink
opts.SourceType = kiwi.RepoSourceTypeMetalink
}

if source == "" {
return fmt.Errorf("rpm-repo %#q has neither base-uri nor metalink", name)
}

if err := runner.AddRemoteRepo(source, opts); err != nil {
return fmt.Errorf("invalid repo source for %#q:\n%w", name, err)
}

return nil
}

// linkImageArtifacts hard-links the final image artifacts from the work directory to the
// output directory. Uses symlinks to avoid duplicating large image files.
// It parses kiwi's result JSON to determine which files are artifacts.
Expand Down
128 changes: 128 additions & 0 deletions internal/app/azldev/cmds/image/build_repos_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package image

import (
"context"
"os/exec"
"strings"
"testing"

"github.com/microsoft/azure-linux-dev-tools/internal/global/testctx"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/utils/kiwi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const addRepoFlag = "--add-repo"

// captureKiwiAddRepoArgs builds a kiwi.Runner against the given setup function and
// returns the list of `--add-repo <arg>` values that the kiwi command would receive.
func captureKiwiAddRepoArgs(t *testing.T, setup func(*kiwi.Runner) error) []string {
t.Helper()

ctx := testctx.NewCtx()

var captured []string

ctx.CmdFactory.RunHandler = func(cmd *exec.Cmd) error {
captured = cmd.Args

return nil
}

runner := kiwi.NewRunner(ctx, "/description").WithTargetDir("/output")

require.NoError(t, setup(runner))
require.NoError(t, runner.Build(context.Background()))

var args []string

for i, a := range captured {
if a == addRepoFlag && i+1 < len(captured) {
args = append(args, captured[i+1])
}
}

return args
}

func TestAddKiwiRepoFromResource_BaseURI(t *testing.T) {
t.Parallel()

args := captureKiwiAddRepoArgs(t, func(r *kiwi.Runner) error {
return addKiwiRepoFromResource(r, "test-repo", &projectconfig.RpmRepoResource{
BaseURI: "https://example.com/repo/$basearch",
DisableGPGCheck: true,
})
})

require.Len(t, args, 1)
parts := strings.Split(args[0], ",")

// Positional fields per kiwi: source,rpm-md,alias,priority,imageinclude,
// package_gpgcheck,signing_keys,components,distribution,repo_gpgcheck,repo_sourcetype.
assert.Equal(t, "https://example.com/repo/$basearch", parts[0])
assert.Equal(t, "rpm-md", parts[1])
assert.Equal(t, "test-repo", parts[2], "alias must be the repo name (used as-is for kiwi)")
// Priority 50 = remote default.
assert.Equal(t, "50", parts[3])

// disable-gpg-check=true must turn OFF *both* package_gpgcheck (field 6, index 5)
// and repo_gpgcheck (field 10, index 9) — they correspond to fields[1] and fields[5]
// after the first 4 positional fields.
assert.Equal(t, "false", parts[5], "package_gpgcheck must be false when disable-gpg-check=true (field 6 / index 5)")
require.GreaterOrEqual(t, len(parts), 10, "expected at least 10 fields when disable-gpg-check=true: %v", parts)
assert.Equal(t, "false", parts[9], "repo_gpgcheck must be false when disable-gpg-check=true (field 10 / index 9)")
}

func TestAddKiwiRepoFromResource_GPGEnabled_BothChecksOn(t *testing.T) {
t.Parallel()

args := captureKiwiAddRepoArgs(t, func(r *kiwi.Runner) error {
return addKiwiRepoFromResource(r, "signed", &projectconfig.RpmRepoResource{
BaseURI: "https://example.com/repo",
GPGKey: "https://example.com/key.gpg",
})
})

require.Len(t, args, 1)
// The "false" sentinel for either gpgcheck field must NOT appear when GPG is enabled.
// (kiwi defaults both to true; we only emit "false" overrides.)
assert.NotContains(t, args[0], "false",
"no GPG-disable override should appear when DisableGPGCheck=false (got %q)", args[0])
// Signing key must be wrapped in {} braces.
assert.Contains(t, args[0], "{https://example.com/key.gpg}",
"signing key must be projected as `{key}` in field 7 (got %q)", args[0])
}

func TestAddKiwiRepoFromResource_Metalink(t *testing.T) {
t.Parallel()

args := captureKiwiAddRepoArgs(t, func(r *kiwi.Runner) error {
return addKiwiRepoFromResource(r, "ml", &projectconfig.RpmRepoResource{
Metalink: "https://mirrors.example.com/metalink?repo=foo",
DisableGPGCheck: true,
})
})

require.Len(t, args, 1)
assert.True(t, strings.HasPrefix(args[0], "https://mirrors.example.com/metalink?repo=foo,rpm-md,ml,"),
"metalink must be used as the source (got %q)", args[0])
assert.True(t, strings.HasSuffix(args[0], ",metalink"),
"sourcetype trailing field must be `metalink` (got %q)", args[0])
}

func TestAddKiwiRepoFromResource_NoSourceErrors(t *testing.T) {
t.Parallel()

ctx := testctx.NewCtx()
runner := kiwi.NewRunner(ctx, "/description")
err := addKiwiRepoFromResource(runner, "broken", &projectconfig.RpmRepoResource{
DisableGPGCheck: true,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "neither base-uri nor metalink")
}
11 changes: 10 additions & 1 deletion internal/app/azldev/core/buildenvfactory/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"

"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/mockconfig"
"github.com/microsoft/azure-linux-dev-tools/internal/buildenv"
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
)
Expand Down Expand Up @@ -59,8 +60,16 @@ func NewMockRootFactoryForEnv(env *azldev.Env) (*buildenv.MockRootFactory, error
return nil, fmt.Errorf("failed to access configured mock config file '%s':\n%w", mockConfigPath, statErr)
}

// Stage a per-build configdir that injects the TOML-derived RPM repos via a
// generated site-defaults.cfg. This replaces hardcoded `[base]`/`[sdk]`/etc.
// blocks in the chroot template with a Jinja loop driven by `azl_repos`.
preparedConfigPath, err := mockconfig.PrepareForRPMBuild(env)
if err != nil {
return nil, fmt.Errorf("failed to prepare mock config:\n%w", err)
}

// Get set up with mock.
factory, err := buildenv.NewMockRootFactory(env, mockConfigPath)
factory, err := buildenv.NewMockRootFactory(env, preparedConfigPath)
if err != nil {
return nil, fmt.Errorf("failed to create mock root factory:\n%w", err)
}
Expand Down
Loading
Loading