Skip to content
Closed
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
11 changes: 11 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ A new `KubeSpanConfig` document has been introduced to configure KubeSpan settin
It replaces and deprecates the previous method of configuring KubeSpan via the `.machine.network.kubespan` field.

The old configuration field will continue to work for backward compatibility.
"""

[notes.link_alias_config]
title = "LinkAliasConfig Selector Enhancements"
description = """\
`LinkAliasConfig` selector now supports two new options:

- `requireUniqueMatch` (default: `true`): When `false`, uses the first matching link instead of requiring exactly one match.
- `skipAliasedLinks` (default: `false`): When `true`, skips links already aliased by a previous `LinkAliasConfig`.

This enables creating stable aliases like `net0`, `net1` from any N links, useful for `BondConfig` and `BridgeConfig` member interfaces on varying hardware.
"""

[notes.extraArgs]
Expand Down
24 changes: 21 additions & 3 deletions internal/app/machined/pkg/controllers/network/link_alias_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ func (ctrl *LinkAliasConfigController) Run(ctx context.Context, r controller.Run
var matchedLinks []*network.LinkStatus

for idx, link := range physicalLinkSpecs {
// Skip links that already have an alias if skipAliasedLinks is enabled
if lac.SkipAliasedLinks() {
if _, ok := linkAliases[physicalLinks[idx].Metadata().ID()]; ok {
continue
}
}

matches, err := lac.LinkSelector().EvalBool(celenv.LinkLocator(), map[string]any{
"link": link,
})
Expand All @@ -130,15 +137,26 @@ func (ctrl *LinkAliasConfigController) Run(ctx context.Context, r controller.Run
}

if len(matchedLinks) > 1 {
logger.Warn("link selector matched multiple links, skipping",
if lac.RequireUniqueMatch() {
logger.Warn("link selector matched multiple links, skipping",
zap.String("selector", lac.LinkSelector().String()),
zap.String("alias", lac.Name()),
zap.Strings("links", xslices.Map(matchedLinks, func(item *network.LinkStatus) string {
return item.Metadata().ID()
})),
)

continue
}

logger.Info("link selector matched multiple links, using first match",
zap.String("selector", lac.LinkSelector().String()),
zap.String("alias", lac.Name()),
zap.String("selected_link", matchedLinks[0].Metadata().ID()),
zap.Strings("links", xslices.Map(matchedLinks, func(item *network.LinkStatus) string {
return item.Metadata().ID()
})),
)

continue
}

matchedLink := matchedLinks[0]
Expand Down
150 changes: 123 additions & 27 deletions internal/app/machined/pkg/controllers/network/link_alias_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/cosi-project/runtime/pkg/resource/rtestutils"
"github.com/siderolabs/go-pointer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"

Expand All @@ -29,6 +30,25 @@ type LinkAliasConfigSuite struct {
ctest.DefaultSuite
}

type testLink struct {
name string
permanentAddr string
}

func (suite *LinkAliasConfigSuite) createLinks(links []testLink) {
for _, link := range links {
pAddr, err := net.ParseMAC(link.permanentAddr)
suite.Require().NoError(err)

status := network.NewLinkStatus(network.NamespaceName, link.name)
status.TypedSpec().PermanentAddr = nethelpers.HardwareAddr(pAddr)
status.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(pAddr)
status.TypedSpec().Type = nethelpers.LinkEther

suite.Create(status)
}
}

func (suite *LinkAliasConfigSuite) TestMachineConfigurationNewStyle() {
lc1 := networkcfg.NewLinkAliasConfigV1Alpha1("net0")
lc1.Selector.Match = cel.MustExpression(cel.ParseBooleanExpression(`glob("00:1a:2b:*", mac(link.permanent_addr))`, celenv.LinkLocator()))
Expand All @@ -42,33 +62,11 @@ func (suite *LinkAliasConfigSuite) TestMachineConfigurationNewStyle() {
cfg := config.NewMachineConfig(ctr)
suite.Create(cfg)

for _, link := range []struct {
name string
permanentAddr string
}{
{
name: "enp0s2",
permanentAddr: "00:1a:2b:33:44:55",
},
{
name: "enp1s3",
permanentAddr: "33:44:55:66:77:88",
},
{
name: "enp1s4",
permanentAddr: "33:44:55:66:77:89",
},
} {
pAddr, err := net.ParseMAC(link.permanentAddr)
suite.Require().NoError(err)

status := network.NewLinkStatus(network.NamespaceName, link.name)
status.TypedSpec().PermanentAddr = nethelpers.HardwareAddr(pAddr)
status.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(pAddr)
status.TypedSpec().Type = nethelpers.LinkEther

suite.Create(status)
}
suite.createLinks([]testLink{
{name: "enp0s2", permanentAddr: "00:1a:2b:33:44:55"},
{name: "enp1s3", permanentAddr: "33:44:55:66:77:88"},
{name: "enp1s4", permanentAddr: "33:44:55:66:77:89"},
})

rtestutils.AssertResource(suite.Ctx(), suite.T(), suite.State(), "enp0s2", func(spec *network.LinkAliasSpec, asrt *assert.Assertions) {
asrt.Equal("net0", spec.TypedSpec().Alias)
Expand All @@ -81,6 +79,104 @@ func (suite *LinkAliasConfigSuite) TestMachineConfigurationNewStyle() {
rtestutils.AssertNoResource[*network.LinkAliasSpec](suite.Ctx(), suite.T(), suite.State(), "enp0s2")
}

func (suite *LinkAliasConfigSuite) TestRequireUniqueMatchFalse() {
// Test that when requireUniqueMatch is false, the first matching link is used
lc1 := networkcfg.NewLinkAliasConfigV1Alpha1("net0")
lc1.Selector.Match = cel.MustExpression(cel.ParseBooleanExpression(`glob("33:44:55:*", mac(link.permanent_addr))`, celenv.LinkLocator()))
lc1.Selector.RequireUniqueMatch = pointer.To(false) // Allow multiple matches, use first

ctr, err := container.New(lc1)
suite.Require().NoError(err)

cfg := config.NewMachineConfig(ctr)
suite.Create(cfg)

suite.createLinks([]testLink{
{name: "enp1s3", permanentAddr: "33:44:55:66:77:88"},
{name: "enp1s4", permanentAddr: "33:44:55:66:77:89"},
})

// First link (enp1s3) should get the alias since it's first in iteration order
rtestutils.AssertResource(suite.Ctx(), suite.T(), suite.State(), "enp1s3", func(spec *network.LinkAliasSpec, asrt *assert.Assertions) {
asrt.Equal("net0", spec.TypedSpec().Alias)
})
rtestutils.AssertNoResource[*network.LinkAliasSpec](suite.Ctx(), suite.T(), suite.State(), "enp1s4")

suite.Destroy(cfg)
}

func (suite *LinkAliasConfigSuite) TestSkipAliasedLinks() {
// Test that skipAliasedLinks allows creating net0 and net1 from any two links
lc1 := networkcfg.NewLinkAliasConfigV1Alpha1("net0")
lc1.Selector.Match = cel.MustExpression(cel.ParseBooleanExpression(`link.type == 1`, celenv.LinkLocator())) // Match all ethernet links
lc1.Selector.RequireUniqueMatch = pointer.To(false)
lc1.Selector.SkipAliasedLinks = pointer.To(false) // First config doesn't need to skip

lc2 := networkcfg.NewLinkAliasConfigV1Alpha1("net1")
lc2.Selector.Match = cel.MustExpression(cel.ParseBooleanExpression(`link.type == 1`, celenv.LinkLocator())) // Same selector
lc2.Selector.RequireUniqueMatch = pointer.To(false)
lc2.Selector.SkipAliasedLinks = pointer.To(true) // Skip links already aliased

ctr, err := container.New(lc1, lc2)
suite.Require().NoError(err)

cfg := config.NewMachineConfig(ctr)
suite.Create(cfg)

suite.createLinks([]testLink{
{name: "enp0s2", permanentAddr: "00:1a:2b:33:44:55"},
{name: "enp1s3", permanentAddr: "33:44:55:66:77:88"},
{name: "enp1s4", permanentAddr: "33:44:55:66:77:89"},
})

// First link gets net0, second link gets net1
rtestutils.AssertResource(suite.Ctx(), suite.T(), suite.State(), "enp0s2", func(spec *network.LinkAliasSpec, asrt *assert.Assertions) {
asrt.Equal("net0", spec.TypedSpec().Alias)
})
rtestutils.AssertResource(suite.Ctx(), suite.T(), suite.State(), "enp1s3", func(spec *network.LinkAliasSpec, asrt *assert.Assertions) {
asrt.Equal("net1", spec.TypedSpec().Alias)
})
// Third link doesn't get an alias
rtestutils.AssertNoResource[*network.LinkAliasSpec](suite.Ctx(), suite.T(), suite.State(), "enp1s4")

suite.Destroy(cfg)
}

func (suite *LinkAliasConfigSuite) TestSkipAliasedLinksWithUniqueMatch() {
// Test requireUniqueMatch=true (default) + skipAliasedLinks=true
// First config matches exactly one link, second config skips the aliased link and matches exactly one remaining link
lc1 := networkcfg.NewLinkAliasConfigV1Alpha1("net0")
lc1.Selector.Match = cel.MustExpression(cel.ParseBooleanExpression(`mac(link.permanent_addr) == "00:1a:2b:33:44:55"`, celenv.LinkLocator()))
// requireUniqueMatch defaults to true

lc2 := networkcfg.NewLinkAliasConfigV1Alpha1("net1")
lc2.Selector.Match = cel.MustExpression(cel.ParseBooleanExpression(`glob("*", mac(link.permanent_addr))`, celenv.LinkLocator())) // Matches all links
// requireUniqueMatch defaults to true
lc2.Selector.SkipAliasedLinks = pointer.To(true) // Skip enp0s2 which got net0

ctr, err := container.New(lc1, lc2)
suite.Require().NoError(err)

cfg := config.NewMachineConfig(ctr)
suite.Create(cfg)

suite.createLinks([]testLink{
{name: "enp0s2", permanentAddr: "00:1a:2b:33:44:55"},
{name: "enp1s3", permanentAddr: "33:44:55:66:77:88"},
})

// First link gets net0 (exact match)
rtestutils.AssertResource(suite.Ctx(), suite.T(), suite.State(), "enp0s2", func(spec *network.LinkAliasSpec, asrt *assert.Assertions) {
asrt.Equal("net0", spec.TypedSpec().Alias)
})
// Second link gets net1 (enp0s2 skipped due to skipAliasedLinks, leaving only enp1s3 as unique match)
rtestutils.AssertResource(suite.Ctx(), suite.T(), suite.State(), "enp1s3", func(spec *network.LinkAliasSpec, asrt *assert.Assertions) {
asrt.Equal("net1", spec.TypedSpec().Alias)
})

suite.Destroy(cfg)
}

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

Expand Down
5 changes: 5 additions & 0 deletions pkg/machinery/config/config/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ type NetworkRouteConfig interface {
type NetworkLinkAliasConfig interface {
NamedDocument
LinkSelector() cel.Expression
// RequireUniqueMatch returns true if the selector must match exactly one link.
// When false, if multiple links match, the first matching link is used.
RequireUniqueMatch() bool
// SkipAliasedLinks returns true if links that already have an alias should be skipped.
SkipAliasedLinks() bool
}

// NetworkDHCPConfig defines a DHCP configuration for a network link.
Expand Down
20 changes: 17 additions & 3 deletions pkg/machinery/config/schemas/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2376,9 +2376,9 @@
"selector": {
"$ref": "#/$defs/network.LinkSelector",
"title": "selector",
"description": "Selector to match the link to alias.\n\nSelector must match exactly one link, otherwise an error is returned.\nIf multiple selectors match the same link, the first one is used.\n",
"markdownDescription": "Selector to match the link to alias.\n\nSelector must match exactly one link, otherwise an error is returned.\nIf multiple selectors match the same link, the first one is used.",
"x-intellij-html-description": "\u003cp\u003eSelector to match the link to alias.\u003c/p\u003e\n\n\u003cp\u003eSelector must match exactly one link, otherwise an error is returned.\nIf multiple selectors match the same link, the first one is used.\u003c/p\u003e\n"
"description": "Selector to match the link to alias.\n\nBy default, the selector must match exactly one link, otherwise the alias is not applied.\nSet requireUniqueMatch to false to allow multiple matches and use the first matching link.\nIf multiple selectors match the same link, the first one is used.\n",
"markdownDescription": "Selector to match the link to alias.\n\nBy default, the selector must match exactly one link, otherwise the alias is not applied.\nSet `requireUniqueMatch` to `false` to allow multiple matches and use the first matching link.\nIf multiple selectors match the same link, the first one is used.",
"x-intellij-html-description": "\u003cp\u003eSelector to match the link to alias.\u003c/p\u003e\n\n\u003cp\u003eBy default, the selector must match exactly one link, otherwise the alias is not applied.\nSet \u003ccode\u003erequireUniqueMatch\u003c/code\u003e to \u003ccode\u003efalse\u003c/code\u003e to allow multiple matches and use the first matching link.\nIf multiple selectors match the same link, the first one is used.\u003c/p\u003e\n"
}
},
"additionalProperties": false,
Expand Down Expand Up @@ -2476,6 +2476,20 @@
"description": "The Common Expression Language (CEL) expression to match the link.\n",
"markdownDescription": "The Common Expression Language (CEL) expression to match the link.",
"x-intellij-html-description": "\u003cp\u003eThe Common Expression Language (CEL) expression to match the link.\u003c/p\u003e\n"
},
"requireUniqueMatch": {
"type": "boolean",
"title": "requireUniqueMatch",
"description": "Require the selector to match exactly one link.\n\nWhen set to false, if multiple links match the selector, the first matching link is used.\nWhen set to true (default), if multiple links match, the alias is not applied.\n",
"markdownDescription": "Require the selector to match exactly one link.\n\nWhen set to `false`, if multiple links match the selector, the first matching link is used.\nWhen set to `true` (default), if multiple links match, the alias is not applied.",
"x-intellij-html-description": "\u003cp\u003eRequire the selector to match exactly one link.\u003c/p\u003e\n\n\u003cp\u003eWhen set to \u003ccode\u003efalse\u003c/code\u003e, if multiple links match the selector, the first matching link is used.\nWhen set to \u003ccode\u003etrue\u003c/code\u003e (default), if multiple links match, the alias is not applied.\u003c/p\u003e\n"
},
"skipAliasedLinks": {
"type": "boolean",
"title": "skipAliasedLinks",
"description": "Skip links that already have an alias assigned by a previous LinkAliasConfig.\n\nThis allows creating sequential aliases like net0 and net1 from any N links\nby using the same broad selector and relying on processing order.\n",
"markdownDescription": "Skip links that already have an alias assigned by a previous LinkAliasConfig.\n\nThis allows creating sequential aliases like `net0` and `net1` from any N links\nby using the same broad selector and relying on processing order.",
"x-intellij-html-description": "\u003cp\u003eSkip links that already have an alias assigned by a previous LinkAliasConfig.\u003c/p\u003e\n\n\u003cp\u003eThis allows creating sequential aliases like \u003ccode\u003enet0\u003c/code\u003e and \u003ccode\u003enet1\u003c/code\u003e from any N links\nby using the same broad selector and relying on processing order.\u003c/p\u003e\n"
}
},
"additionalProperties": false,
Expand Down
8 changes: 8 additions & 0 deletions pkg/machinery/config/types/network/deep_copy.generated.go

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

37 changes: 36 additions & 1 deletion pkg/machinery/config/types/network/link_alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ type LinkAliasConfigV1Alpha1 struct {
// description: |
// Selector to match the link to alias.
//
// Selector must match exactly one link, otherwise an error is returned.
// By default, the selector must match exactly one link, otherwise the alias is not applied.
// Set `requireUniqueMatch` to `false` to allow multiple matches and use the first matching link.
// If multiple selectors match the same link, the first one is used.
Selector LinkSelector `yaml:"selector,omitempty"`
}
Expand All @@ -87,6 +88,22 @@ type LinkSelector struct {
// exampleLinkSelector3()
// name: match links by driver name
Match cel.Expression `yaml:"match,omitempty"`
// description: |
// Require the selector to match exactly one link.
//
// When set to `false`, if multiple links match the selector, the first matching link is used.
// When set to `true` (default), if multiple links match, the alias is not applied.
// schema:
// type: boolean
RequireUniqueMatch *bool `yaml:"requireUniqueMatch,omitempty"`
// description: |
// Skip links that already have an alias assigned by a previous LinkAliasConfig.
//
// This allows creating sequential aliases like `net0` and `net1` from any N links
// by using the same broad selector and relying on processing order.
// schema:
// type: boolean
SkipAliasedLinks *bool `yaml:"skipAliasedLinks,omitempty"`
}

// NewLinkAliasConfigV1Alpha1 creates a new LinkAliasConfig config document.
Expand Down Expand Up @@ -155,3 +172,21 @@ func (s *LinkAliasConfigV1Alpha1) Validate(validation.RuntimeMode, ...validation
func (s *LinkAliasConfigV1Alpha1) LinkSelector() cel.Expression {
return s.Selector.Match
}

// RequireUniqueMatch implements config.NetworkLinkAliasConfig interface.
func (s *LinkAliasConfigV1Alpha1) RequireUniqueMatch() bool {
if s.Selector.RequireUniqueMatch == nil {
return true
}

return *s.Selector.RequireUniqueMatch
}

// SkipAliasedLinks implements config.NetworkLinkAliasConfig interface.
func (s *LinkAliasConfigV1Alpha1) SkipAliasedLinks() bool {
if s.Selector.SkipAliasedLinks == nil {
return false
}

return *s.Selector.SkipAliasedLinks
}
Loading