Skip to content

Commit 9a42b05

Browse files
committed
feat: implement link aliasing
Fixes #10956 Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
1 parent d732bd0 commit 9a42b05

File tree

36 files changed

+1756
-364
lines changed

36 files changed

+1756
-364
lines changed

api/resource/definitions/network/network.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ message HostnameStatusSpec {
213213
string domainname = 2;
214214
}
215215

216+
// LinkAliasSpecSpec describes status of rendered secrets.
217+
message LinkAliasSpecSpec {
218+
string alias = 1;
219+
}
220+
216221
// LinkRefreshSpec describes status of rendered secrets.
217222
message LinkRefreshSpec {
218223
int64 generation = 1;
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package network
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"slices"
11+
12+
"github.com/cosi-project/runtime/pkg/controller"
13+
"github.com/cosi-project/runtime/pkg/safe"
14+
"github.com/cosi-project/runtime/pkg/state"
15+
"github.com/siderolabs/gen/optional"
16+
"github.com/siderolabs/gen/xslices"
17+
"go.uber.org/zap"
18+
19+
networkpb "github.com/siderolabs/talos/pkg/machinery/api/resource/definitions/network"
20+
"github.com/siderolabs/talos/pkg/machinery/cel/celenv"
21+
configconfig "github.com/siderolabs/talos/pkg/machinery/config/config"
22+
"github.com/siderolabs/talos/pkg/machinery/proto"
23+
"github.com/siderolabs/talos/pkg/machinery/resources/config"
24+
"github.com/siderolabs/talos/pkg/machinery/resources/network"
25+
)
26+
27+
// LinkAliasConfigController manages network.LinkAliasSpec based on machine configuration, list of links, etc.
28+
type LinkAliasConfigController struct{}
29+
30+
// Name implements controller.Controller interface.
31+
func (ctrl *LinkAliasConfigController) Name() string {
32+
return "network.LinkAliasConfigController"
33+
}
34+
35+
// Inputs implements controller.Controller interface.
36+
func (ctrl *LinkAliasConfigController) Inputs() []controller.Input {
37+
return []controller.Input{
38+
{
39+
Namespace: config.NamespaceName,
40+
Type: config.MachineConfigType,
41+
ID: optional.Some(config.ActiveID),
42+
Kind: controller.InputWeak,
43+
},
44+
{
45+
Namespace: network.NamespaceName,
46+
Type: network.LinkStatusType,
47+
Kind: controller.InputWeak,
48+
},
49+
}
50+
}
51+
52+
// Outputs implements controller.Controller interface.
53+
func (ctrl *LinkAliasConfigController) Outputs() []controller.Output {
54+
return []controller.Output{
55+
{
56+
Type: network.LinkAliasSpecType,
57+
Kind: controller.OutputExclusive,
58+
},
59+
}
60+
}
61+
62+
// Run implements controller.Controller interface.
63+
//
64+
//nolint:gocyclo,cyclop
65+
func (ctrl *LinkAliasConfigController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
66+
for {
67+
select {
68+
case <-ctx.Done():
69+
return nil
70+
case <-r.EventCh():
71+
}
72+
73+
r.StartTrackingOutputs()
74+
75+
cfg, err := safe.ReaderGetByID[*config.MachineConfig](ctx, r, config.ActiveID)
76+
if err != nil {
77+
if !state.IsNotFoundError(err) {
78+
return fmt.Errorf("error getting machine config: %w", err)
79+
}
80+
}
81+
82+
linkStatuses, err := safe.ReaderListAll[*network.LinkStatus](ctx, r)
83+
if err != nil {
84+
return fmt.Errorf("error listing link statuses: %w", err)
85+
}
86+
87+
// we are only interested in physical links
88+
physicalLinks := xslices.Filter(slices.Collect(linkStatuses.All()), func(item *network.LinkStatus) bool {
89+
return item.TypedSpec().Physical()
90+
})
91+
92+
physicalLinkSpecs := make([]*networkpb.LinkStatusSpec, 0, len(physicalLinks))
93+
94+
for _, link := range physicalLinks {
95+
var spec networkpb.LinkStatusSpec
96+
97+
if err = proto.ResourceSpecToProto(link, &spec); err != nil {
98+
return fmt.Errorf("error converting link spec (%s) to proto: %w", link.Metadata().ID(), err)
99+
}
100+
101+
physicalLinkSpecs = append(physicalLinkSpecs, &spec)
102+
}
103+
104+
var linkAliasConfigs []configconfig.NetworkLinkAliasConfig
105+
106+
if cfg != nil {
107+
linkAliasConfigs = cfg.Config().NetworkLinkAliasConfigs()
108+
}
109+
110+
linkAliases := map[string]string{}
111+
112+
for _, lac := range linkAliasConfigs {
113+
var matchedLinks []*network.LinkStatus
114+
115+
for idx, link := range physicalLinkSpecs {
116+
matches, err := lac.LinkSelector().EvalBool(celenv.LinkLocator(), map[string]any{
117+
"link": link,
118+
})
119+
if err != nil {
120+
return fmt.Errorf("error evaluating link selector: %w", err)
121+
}
122+
123+
if matches {
124+
matchedLinks = append(matchedLinks, physicalLinks[idx])
125+
}
126+
}
127+
128+
if len(matchedLinks) == 0 {
129+
continue
130+
}
131+
132+
if len(matchedLinks) > 1 {
133+
logger.Warn("link selector matched multiple links, skipping",
134+
zap.String("selector", lac.LinkSelector().String()),
135+
zap.String("alias", lac.Name()),
136+
zap.Strings("links", xslices.Map(matchedLinks, func(item *network.LinkStatus) string {
137+
return item.Metadata().ID()
138+
})),
139+
)
140+
141+
continue
142+
}
143+
144+
matchedLink := matchedLinks[0]
145+
146+
if _, ok := linkAliases[matchedLink.Metadata().ID()]; ok {
147+
logger.Warn("link already has an alias, skipping",
148+
zap.String("link", matchedLink.Metadata().ID()),
149+
zap.String("existing_alias", linkAliases[matchedLink.Metadata().ID()]),
150+
zap.String("new_alias", lac.Name()),
151+
)
152+
153+
continue
154+
}
155+
156+
linkAliases[matchedLink.Metadata().ID()] = lac.Name()
157+
}
158+
159+
for linkID, alias := range linkAliases {
160+
if err = safe.WriterModify(
161+
ctx,
162+
r,
163+
network.NewLinkAliasSpec(network.NamespaceName, linkID),
164+
func(r *network.LinkAliasSpec) error {
165+
r.TypedSpec().Alias = alias
166+
167+
return nil
168+
},
169+
); err != nil {
170+
return fmt.Errorf("error writing link alias spec for link %q: %w", linkID, err)
171+
}
172+
}
173+
174+
if err := safe.CleanupOutputs[*network.LinkAliasSpec](ctx, r); err != nil {
175+
return fmt.Errorf("error cleaning up link alias specs: %w", err)
176+
}
177+
}
178+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
//nolint:goconst
6+
package network_test
7+
8+
import (
9+
"net"
10+
"testing"
11+
"time"
12+
13+
"github.com/cosi-project/runtime/pkg/resource/rtestutils"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/suite"
16+
17+
"github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest"
18+
netctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network"
19+
"github.com/siderolabs/talos/pkg/machinery/cel"
20+
"github.com/siderolabs/talos/pkg/machinery/cel/celenv"
21+
"github.com/siderolabs/talos/pkg/machinery/config/container"
22+
networkcfg "github.com/siderolabs/talos/pkg/machinery/config/types/network"
23+
"github.com/siderolabs/talos/pkg/machinery/nethelpers"
24+
"github.com/siderolabs/talos/pkg/machinery/resources/config"
25+
"github.com/siderolabs/talos/pkg/machinery/resources/network"
26+
)
27+
28+
type LinkAliasConfigSuite struct {
29+
ctest.DefaultSuite
30+
}
31+
32+
func (suite *LinkAliasConfigSuite) TestMachineConfigurationNewStyle() {
33+
lc1 := networkcfg.NewLinkAliasConfigV1Alpha1("net0")
34+
lc1.Selector.Match = cel.MustExpression(cel.ParseBooleanExpression(`glob("00:1a:2b:*", mac(link.permanent_addr))`, celenv.LinkLocator()))
35+
36+
lc2 := networkcfg.NewLinkAliasConfigV1Alpha1("net1")
37+
lc2.Selector.Match = cel.MustExpression(cel.ParseBooleanExpression(`glob("33:44:55:*", mac(link.permanent_addr))`, celenv.LinkLocator()))
38+
39+
ctr, err := container.New(lc1, lc2)
40+
suite.Require().NoError(err)
41+
42+
cfg := config.NewMachineConfig(ctr)
43+
suite.Create(cfg)
44+
45+
for _, link := range []struct {
46+
name string
47+
permanentAddr string
48+
}{
49+
{
50+
name: "enp0s2",
51+
permanentAddr: "00:1a:2b:33:44:55",
52+
},
53+
{
54+
name: "enp1s3",
55+
permanentAddr: "33:44:55:66:77:88",
56+
},
57+
{
58+
name: "enp1s4",
59+
permanentAddr: "33:44:55:66:77:89",
60+
},
61+
} {
62+
pAddr, err := net.ParseMAC(link.permanentAddr)
63+
suite.Require().NoError(err)
64+
65+
status := network.NewLinkStatus(network.NamespaceName, link.name)
66+
status.TypedSpec().PermanentAddr = nethelpers.HardwareAddr(pAddr)
67+
status.TypedSpec().HardwareAddr = nethelpers.HardwareAddr(pAddr)
68+
status.TypedSpec().Type = nethelpers.LinkEther
69+
70+
suite.Create(status)
71+
}
72+
73+
rtestutils.AssertResource(suite.Ctx(), suite.T(), suite.State(), "enp0s2", func(spec *network.LinkAliasSpec, asrt *assert.Assertions) {
74+
asrt.Equal("net0", spec.TypedSpec().Alias)
75+
})
76+
rtestutils.AssertNoResource[*network.LinkAliasSpec](suite.Ctx(), suite.T(), suite.State(), "enp1s3")
77+
rtestutils.AssertNoResource[*network.LinkAliasSpec](suite.Ctx(), suite.T(), suite.State(), "enp1s4")
78+
79+
suite.Destroy(cfg)
80+
81+
rtestutils.AssertNoResource[*network.LinkAliasSpec](suite.Ctx(), suite.T(), suite.State(), "enp0s2")
82+
}
83+
84+
func TestLinkAliasConfigSuite(t *testing.T) {
85+
t.Parallel()
86+
87+
suite.Run(t, &LinkAliasConfigSuite{
88+
DefaultSuite: ctest.DefaultSuite{
89+
Timeout: 5 * time.Second,
90+
AfterSetup: func(s *ctest.DefaultSuite) {
91+
s.Require().NoError(s.Runtime().RegisterController(&netctrl.LinkAliasConfigController{}))
92+
},
93+
},
94+
})
95+
}

0 commit comments

Comments
 (0)