feat: add bond slaves ordering

Before this change, we didn't preserve bonded interfaces ordering, which caused problems in some scenarios. Fix this by remembering their position in the original config.

Fixes #5207.

Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
This commit is contained in:
Dmitriy Matrenichev 2022-04-28 00:04:08 +04:00
parent 03ef62ad8b
commit 867d38f28f
No known key found for this signature in database
GPG Key ID: D3363CF894E68892
14 changed files with 369 additions and 87 deletions

View File

@ -16,6 +16,7 @@ import (
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/machinery/ordered"
"github.com/talos-systems/talos/pkg/machinery/resources/network"
)
@ -244,13 +245,13 @@ func ParseCmdlineNetwork(cmdline *procfs.Cmdline) (CmdlineNetworking, error) {
linkSpecSpecs = append(linkSpecSpecs, bondLinkSpec)
for _, slave := range bondSlaves {
for idx, slave := range bondSlaves {
slaveLinkSpec := network.LinkSpecSpec{
Name: slave,
Up: true,
ConfigLayer: network.ConfigCmdline,
}
SetBondSlave(&slaveLinkSpec, bondName)
SetBondSlave(&slaveLinkSpec, ordered.MakePair(bondName, idx))
linkSpecSpecs = append(linkSpecSpecs, slaveLinkSpec)
}
}

View File

@ -63,14 +63,20 @@ func (suite *CmdlineSuite) TestParse() {
Up: true,
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
MasterName: "bond0",
BondSlave: netconfig.BondSlave{
MasterName: "bond0",
SlaveIndex: 0,
},
},
{
Name: "eth1",
Up: true,
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
MasterName: "bond0",
BondSlave: netconfig.BondSlave{
MasterName: "bond0",
SlaveIndex: 1,
},
},
},
}
@ -233,14 +239,20 @@ func (suite *CmdlineSuite) TestParse() {
Up: true,
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
MasterName: "bond1",
BondSlave: netconfig.BondSlave{
MasterName: "bond1",
SlaveIndex: 0,
},
},
{
Name: "eth4",
Up: true,
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
MasterName: "bond1",
BondSlave: netconfig.BondSlave{
MasterName: "bond1",
SlaveIndex: 1,
},
},
},
},
@ -275,14 +287,20 @@ func (suite *CmdlineSuite) TestParse() {
Up: true,
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
MasterName: "bond1",
BondSlave: netconfig.BondSlave{
MasterName: "bond1",
SlaveIndex: 0,
},
},
{
Name: "eth4",
Up: true,
Logical: false,
ConfigLayer: netconfig.ConfigCmdline,
MasterName: "bond1",
BondSlave: netconfig.BondSlave{
MasterName: "bond1",
SlaveIndex: 1,
},
},
},
},

View File

@ -18,6 +18,7 @@ import (
talosconfig "github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/machinery/nethelpers"
"github.com/talos-systems/talos/pkg/machinery/ordered"
"github.com/talos-systems/talos/pkg/machinery/resources/config"
"github.com/talos-systems/talos/pkg/machinery/resources/network"
)
@ -269,7 +270,7 @@ func (ctrl *LinkConfigController) parseCmdline(logger *zap.Logger) ([]network.Li
//nolint:gocyclo
func (ctrl *LinkConfigController) parseMachineConfiguration(logger *zap.Logger, cfgProvider talosconfig.Provider) []network.LinkSpecSpec {
// scan for the bonds
bondedLinks := map[string]string{} // mapping physical interface -> bond interface
bondedLinks := map[string]ordered.Pair[string, int]{} // mapping physical interface -> bond interface
for _, device := range cfgProvider.Machine().Network().Devices() {
if device.Ignore() {
@ -280,12 +281,12 @@ func (ctrl *LinkConfigController) parseMachineConfiguration(logger *zap.Logger,
continue
}
for _, linkName := range device.Bond().Interfaces() {
if bondName, exists := bondedLinks[linkName]; exists && bondName != device.Interface() {
for idx, linkName := range device.Bond().Interfaces() {
if bondData, exists := bondedLinks[linkName]; exists && bondData.F1 != device.Interface() {
logger.Sugar().Warnf("link %q is included into more than two bonds", linkName)
}
bondedLinks[linkName] = device.Interface()
bondedLinks[linkName] = ordered.MakePair(device.Interface(), idx)
}
}
@ -331,7 +332,7 @@ func (ctrl *LinkConfigController) parseMachineConfiguration(logger *zap.Logger,
}
}
for slaveName, bondName := range bondedLinks {
for slaveName, bondData := range bondedLinks {
if _, exists := linkMap[slaveName]; !exists {
linkMap[slaveName] = &network.LinkSpecSpec{
Name: slaveName,
@ -340,7 +341,7 @@ func (ctrl *LinkConfigController) parseMachineConfiguration(logger *zap.Logger,
}
}
SetBondSlave(linkMap[slaveName], bondName)
SetBondSlave(linkMap[slaveName], bondData)
}
links := make([]network.LinkSpecSpec, 0, len(linkMap))

View File

@ -316,7 +316,7 @@ func (suite *LinkConfigSuite) TestMachineConfiguration() {
case "eth2", "eth3":
suite.Assert().True(r.TypedSpec().Up)
suite.Assert().False(r.TypedSpec().Logical)
suite.Assert().Equal("bond0", r.TypedSpec().MasterName)
suite.Assert().Equal("bond0", r.TypedSpec().BondSlave.MasterName)
case "bond0":
suite.Assert().True(r.TypedSpec().Up)
suite.Assert().True(r.TypedSpec().Logical)

View File

@ -7,6 +7,7 @@ package network
import (
"context"
"fmt"
"sort"
"github.com/AlekSi/pointer"
"github.com/cosi-project/runtime/pkg/controller"
@ -20,6 +21,7 @@ import (
networkadapter "github.com/talos-systems/talos/internal/app/machined/pkg/adapters/network"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network/watch"
"github.com/talos-systems/talos/pkg/machinery/nethelpers"
"github.com/talos-systems/talos/pkg/machinery/ordered"
"github.com/talos-systems/talos/pkg/machinery/resources/network"
)
@ -111,6 +113,8 @@ func (ctrl *LinkSpecController) Run(ctx context.Context, r controller.Runtime, l
// loop over links and make reconcile decision
var multiErr *multierror.Error
SortBonds(list.Items)
for _, res := range list.Items {
link := res.(*network.LinkSpec) //nolint:forcetypeassert,errcheck
@ -125,6 +129,27 @@ func (ctrl *LinkSpecController) Run(ctx context.Context, r controller.Runtime, l
}
}
// SortBonds sort resources in increasing order, except it places slave interfaces right after the bond
// in proper order.
func SortBonds(items []resource.Resource) {
sort.Slice(items, func(i, j int) bool {
left := items[i].Spec().(network.LinkSpecSpec) //nolint:errcheck
right := items[j].Spec().(network.LinkSpecSpec) //nolint:errcheck
l := ordered.MakeTriple(left.Name, 0, "")
if left.BondSlave.MasterName != "" {
l = ordered.MakeTriple(left.BondSlave.MasterName, left.BondSlave.SlaveIndex, left.Name)
}
r := ordered.MakeTriple(right.Name, 0, "")
if right.BondSlave.MasterName != "" {
r = ordered.MakeTriple(right.BondSlave.MasterName, right.BondSlave.SlaveIndex, right.Name)
}
return l.LessThan(r)
})
}
func findLink(links []rtnetlink.LinkMessage, name string) *rtnetlink.LinkMessage {
for i, link := range links {
if link.Attributes.Name == name {
@ -341,7 +366,7 @@ func (ctrl *LinkSpecController) syncLink(ctx context.Context, r controller.Runti
Master: pointer.ToUint32(0),
},
}); err != nil {
return fmt.Errorf("error unslaving link %q under %q: %w", slave.Attributes.Name, link.TypedSpec().MasterName, err)
return fmt.Errorf("error unslaving link %q under %q: %w", slave.Attributes.Name, link.TypedSpec().BondSlave.MasterName, err)
}
(*links)[i].Attributes.Master = nil
@ -448,8 +473,8 @@ func (ctrl *LinkSpecController) syncLink(ctx context.Context, r controller.Runti
// sync master index (for links which are bond slaves)
var masterIndex uint32
if link.TypedSpec().MasterName != "" {
if master := findLink(*links, link.TypedSpec().MasterName); master != nil {
if link.TypedSpec().BondSlave.MasterName != "" {
if master := findLink(*links, link.TypedSpec().BondSlave.MasterName); master != nil {
masterIndex = master.Index
}
}
@ -464,12 +489,12 @@ func (ctrl *LinkSpecController) syncLink(ctx context.Context, r controller.Runti
Master: pointer.ToUint32(masterIndex),
},
}); err != nil {
return fmt.Errorf("error enslaving/unslaving link %q under %q: %w", link.TypedSpec().Name, link.TypedSpec().MasterName, err)
return fmt.Errorf("error enslaving/unslaving link %q under %q: %w", link.TypedSpec().Name, link.TypedSpec().BondSlave.MasterName, err)
}
existing.Attributes.Master = pointer.ToUint32(masterIndex)
logger.Info("enslaved/unslaved link", zap.String("parent", link.TypedSpec().MasterName))
logger.Info("enslaved/unslaved link", zap.String("parent", link.TypedSpec().BondSlave.MasterName))
}
}

View File

@ -19,6 +19,7 @@ import (
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/go-retry/retry"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
@ -371,24 +372,30 @@ func (suite *LinkSpecSuite) TestBond() {
dummy0Name := suite.uniqueDummyInterface()
dummy0 := network.NewLinkSpec(network.NamespaceName, dummy0Name)
*dummy0.TypedSpec() = network.LinkSpecSpec{
Name: dummy0Name,
Type: nethelpers.LinkEther,
Kind: "dummy",
Up: true,
Logical: true,
MasterName: bondName,
Name: dummy0Name,
Type: nethelpers.LinkEther,
Kind: "dummy",
Up: true,
Logical: true,
BondSlave: network.BondSlave{
MasterName: bondName,
SlaveIndex: 0,
},
ConfigLayer: network.ConfigDefault,
}
dummy1Name := suite.uniqueDummyInterface()
dummy1 := network.NewLinkSpec(network.NamespaceName, dummy1Name)
*dummy1.TypedSpec() = network.LinkSpecSpec{
Name: dummy1Name,
Type: nethelpers.LinkEther,
Kind: "dummy",
Up: true,
Logical: true,
MasterName: bondName,
Name: dummy1Name,
Type: nethelpers.LinkEther,
Kind: "dummy",
Up: true,
Logical: true,
BondSlave: network.BondSlave{
MasterName: bondName,
SlaveIndex: 1,
},
ConfigLayer: network.ConfigDefault,
}
@ -460,7 +467,7 @@ func (suite *LinkSpecSuite) TestBond() {
// unslave one of the interfaces
_, err = suite.state.UpdateWithConflicts(
suite.ctx, dummy0.Metadata(), func(r resource.Resource) error {
r.(*network.LinkSpec).TypedSpec().MasterName = ""
r.(*network.LinkSpec).TypedSpec().BondSlave.MasterName = ""
return nil
},
@ -533,12 +540,15 @@ func (suite *LinkSpecSuite) TestBond8023ad() {
dummyName := suite.uniqueDummyInterface()
dummy := network.NewLinkSpec(network.NamespaceName, dummyName)
*dummy.TypedSpec() = network.LinkSpecSpec{
Name: dummyName,
Type: nethelpers.LinkEther,
Kind: "dummy",
Up: true,
Logical: true,
MasterName: bondName,
Name: dummyName,
Type: nethelpers.LinkEther,
Kind: "dummy",
Up: true,
Logical: true,
BondSlave: network.BondSlave{
MasterName: bondName,
SlaveIndex: 0,
},
ConfigLayer: network.ConfigDefault,
}
@ -743,3 +753,72 @@ func (suite *LinkSpecSuite) TearDownTest() {
func TestLinkSpecSuite(t *testing.T) {
suite.Run(t, new(LinkSpecSuite))
}
func TestSortBonds(t *testing.T) {
expectedSlice := []network.LinkSpecSpec{
{
Name: "A",
}, {
Name: "G",
BondSlave: network.BondSlave{
MasterName: "A",
SlaveIndex: 0,
},
}, {
Name: "C",
}, {
Name: "E",
BondSlave: network.BondSlave{
MasterName: "C",
SlaveIndex: 0,
},
}, {
Name: "F",
BondSlave: network.BondSlave{
MasterName: "C",
SlaveIndex: 1,
},
}, {
Name: "B",
BondSlave: network.BondSlave{
MasterName: "C",
SlaveIndex: 2,
},
},
}
seed := time.Now().Unix()
rnd := rand.New(rand.NewSource(seed))
for i := 0; i < 100; i++ {
res := toResources(expectedSlice)
rnd.Shuffle(len(res), func(i, j int) { res[i], res[j] = res[j], res[i] })
netctrl.SortBonds(res)
sorted := toSpecs(res)
require.Equal(t, expectedSlice, sorted, "failed with seed %d iteration %d", seed, i)
}
}
func toResources(slice []network.LinkSpecSpec) []resource.Resource {
result := make([]resource.Resource, 0, len(slice))
for _, elem := range slice {
link := network.NewLinkSpec(network.NamespaceName, "bar")
*link.TypedSpec() = elem
result = append(result, link)
}
return result
}
func toSpecs(slice []resource.Resource) []network.LinkSpecSpec {
result := make([]network.LinkSpecSpec, 0, len(slice))
for _, elem := range slice {
v := elem.Spec().(network.LinkSpecSpec) //nolint:errcheck
result = append(result, v)
}
return result
}

View File

@ -11,6 +11,7 @@ import (
networkadapter "github.com/talos-systems/talos/internal/app/machined/pkg/adapters/network"
talosconfig "github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/machinery/nethelpers"
"github.com/talos-systems/talos/pkg/machinery/ordered"
"github.com/talos-systems/talos/pkg/machinery/resources/network"
)
@ -18,8 +19,11 @@ import (
const DefaultRouteMetric = 1024
// SetBondSlave sets the bond slave spec.
func SetBondSlave(link *network.LinkSpecSpec, bondName string) {
link.MasterName = bondName
func SetBondSlave(link *network.LinkSpecSpec, bond ordered.Pair[string, int]) {
link.BondSlave = network.BondSlave{
MasterName: bond.F1,
SlaveIndex: bond.F2,
}
}
// SetBondMaster sets the bond master spec.

View File

@ -117,6 +117,8 @@ func (p *EquinixMetal) ParseMetadata(equinixMetadata *Metadata) (*runtime.Platfo
return nil, fmt.Errorf("error listing host interfaces: %w", err)
}
slaveIndex := 0
for _, iface := range equinixMetadata.Network.Interfaces {
if iface.Bond == "" {
continue
@ -136,11 +138,15 @@ func (p *EquinixMetal) ParseMetadata(equinixMetadata *Metadata) (*runtime.Platfo
networkConfig.Links = append(networkConfig.Links,
network.LinkSpecSpec{
Name: hostIf.Name,
Up: true,
BondSlave: network.BondSlave{
MasterName: bondName,
SlaveIndex: slaveIndex,
},
ConfigLayer: network.ConfigPlatform,
Name: hostIf.Name,
Up: true,
MasterName: bondName,
})
slaveIndex++
break
}
@ -154,8 +160,12 @@ func (p *EquinixMetal) ParseMetadata(equinixMetadata *Metadata) (*runtime.Platfo
ConfigLayer: network.ConfigPlatform,
Name: iface.Name,
Up: true,
MasterName: bondName,
BondSlave: network.BondSlave{
MasterName: bondName,
SlaveIndex: slaveIndex,
},
})
slaveIndex++
}
}

View File

@ -33,6 +33,7 @@ links:
kind: ""
type: netrom
masterName: bond0
slaveIndex: 1
layer: platform
- name: bond0
logical: true

View File

@ -0,0 +1,61 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package ordered
// Ordered is a constraint that permits any ordered type: any type
// that supports the operators < <= >= >.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string
}
// Pair is two element tuple of ordered values.
type Pair[T1, T2 Ordered] struct {
F1 T1
F2 T2
}
// MakePair creates a new Pair.
func MakePair[T1, T2 Ordered](v1 T1, v2 T2) Pair[T1, T2] {
return Pair[T1, T2]{
F1: v1,
F2: v2,
}
}
// Compare returns an integer comparing two pairs in natural order.
// The result will be 0 if p == other, -1 if p < other, and +1 if p > other.
func (p Pair[T1, T2]) Compare(other Pair[T1, T2]) int {
if result := cmp(p.F1, other.F1); result != 0 {
return result
}
return cmp(p.F2, other.F2)
}
// MoreThan checks if current pair is bigger than the other.
func (p Pair[T1, T2]) MoreThan(other Pair[T1, T2]) bool {
return p.Compare(other) == 1
}
// LessThan checks if current pair is lesser than the other.
func (p Pair[T1, T2]) LessThan(other Pair[T1, T2]) bool {
return p.Compare(other) == -1
}
// Equal checks if current pair is equal to the other.
func (p Pair[T1, T2]) Equal(other Pair[T1, T2]) bool {
return p.Compare(other) == 0
}
func cmp[T Ordered](a, b T) int {
switch {
case a == b:
return 0
case a < b:
return -1
default:
return +1
}
}

View File

@ -0,0 +1,46 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package ordered_test
import (
"math"
"math/rand"
"sort"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/talos-systems/talos/pkg/machinery/ordered"
)
func TestTriple(t *testing.T) {
t.Parallel()
expectedSlice := []ordered.Triple[int, string, float64]{
ordered.MakeTriple(math.MinInt64, "Alpha", 69.0),
ordered.MakeTriple(-200, "Alpha", 69.0),
ordered.MakeTriple(-200, "Beta", -69.0),
ordered.MakeTriple(-200, "Beta", 69.0),
ordered.MakeTriple(1, "", 69.0),
ordered.MakeTriple(1, "Alpha", 67.0),
ordered.MakeTriple(1, "Alpha", 68.0),
ordered.MakeTriple(10, "Alpha", 68.0),
ordered.MakeTriple(10, "Beta", 68.0),
ordered.MakeTriple(math.MaxInt64, "", 69.0),
}
seed := time.Now().Unix()
rnd := rand.New(rand.NewSource(seed))
for i := 0; i < 1000; i++ {
a := append([]ordered.Triple[int, string, float64](nil), expectedSlice...)
rnd.Shuffle(len(a), func(i, j int) { a[i], a[j] = a[j], a[i] })
sort.Slice(a, func(i, j int) bool {
return a[i].LessThan(a[j])
})
require.Equal(t, expectedSlice, a, "failed with seed %d iteration %d", seed, i)
}
}

View File

@ -0,0 +1,48 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package ordered
// Triple is three element tuple of ordered values.
type Triple[T1, T2, T3 Ordered] struct {
V1 T1
V2 T2
V3 T3
}
// MakeTriple creates a new Triple.
func MakeTriple[T1, T2, T3 Ordered](v1 T1, v2 T2, v3 T3) Triple[T1, T2, T3] {
return Triple[T1, T2, T3]{
V1: v1,
V2: v2,
V3: v3,
}
}
// Compare returns an integer comparing two triples in natural order.
// The result will be 0 if t == other, -1 if t < other, and +1 if t > other.
func (t Triple[T1, T2, T3]) Compare(other Triple[T1, T2, T3]) int {
if result := cmp(t.V1, other.V1); result != 0 {
return result
} else if result := cmp(t.V2, other.V2); result != 0 {
return result
}
return cmp(t.V3, other.V3)
}
// MoreThan checks if current triple is bigger than the other.
func (t Triple[T1, T2, T3]) MoreThan(other Triple[T1, T2, T3]) bool {
return t.Compare(other) == 1
}
// LessThan checks if current triple is lesser than the other.
func (t Triple[T1, T2, T3]) LessThan(other Triple[T1, T2, T3]) bool {
return t.Compare(other) == -1
}
// Equal checks if current triple is equal to the other.
func (t Triple[T1, T2, T3]) Equal(other Triple[T1, T2, T3]) bool {
return t.Compare(other) == 0
}

View File

@ -43,7 +43,7 @@ type LinkSpecSpec struct {
ParentName string `yaml:"parentName,omitempty"`
// MasterName indicates master link for enslaved bonded interfaces.
MasterName string `yaml:"masterName,omitempty"`
BondSlave BondSlave `yaml:",omitempty,inline"`
// These structures are present depending on "Kind" for Logical intefaces.
VLAN VLANSpec `yaml:"vlan,omitempty"`
@ -54,6 +54,15 @@ type LinkSpecSpec struct {
ConfigLayer ConfigLayer `yaml:"layer"`
}
// BondSlave contains a bond's master name and slave index.
type BondSlave struct {
// MasterName indicates master link for enslaved bonded interfaces.
MasterName string `yaml:"masterName,omitempty"`
// SlaveIndex indicates a slave's position in bond.
SlaveIndex int `yaml:"slaveIndex,omitempty"`
}
// DeepCopy generates a deep copy of LinkSpecSpec.
func (spec LinkSpecSpec) DeepCopy() LinkSpecSpec {
cp := spec
@ -72,51 +81,20 @@ func (spec LinkSpecSpec) DeepCopy() LinkSpecSpec {
return cp
}
var (
zeroVLAN VLANSpec
zeroBondMaster BondMasterSpec
)
// Merge with other, overwriting fields from other if set.
//
//nolint:gocyclo
func (spec *LinkSpecSpec) Merge(other *LinkSpecSpec) error {
// prefer Logical, as it is defined for bonds/vlans, etc.
if other.Logical {
spec.Logical = other.Logical
}
if other.Up {
spec.Up = other.Up
}
if other.MTU != 0 {
spec.MTU = other.MTU
}
if other.Kind != "" {
spec.Kind = other.Kind
}
if other.Type != 0 {
spec.Type = other.Type
}
if other.ParentName != "" {
spec.ParentName = other.ParentName
}
if other.MasterName != "" {
spec.MasterName = other.MasterName
}
if other.VLAN != zeroVLAN {
spec.VLAN = other.VLAN
}
if other.BondMaster != zeroBondMaster {
spec.BondMaster = other.BondMaster
}
updateIfNotZero(&spec.Logical, other.Logical)
updateIfNotZero(&spec.Up, other.Up)
updateIfNotZero(&spec.MTU, other.MTU)
updateIfNotZero(&spec.Kind, other.Kind)
updateIfNotZero(&spec.Type, other.Type)
updateIfNotZero(&spec.ParentName, other.ParentName)
updateIfNotZero(&spec.BondSlave, other.BondSlave)
updateIfNotZero(&spec.VLAN, other.VLAN)
updateIfNotZero(&spec.BondMaster, other.BondMaster)
// Wireguard config should be able to apply non-zero values in earlier config layers which may be zero values in later layers.
// Thus, we handle each Wireguard configuration value discretely.
@ -133,6 +111,13 @@ func (spec *LinkSpecSpec) Merge(other *LinkSpecSpec) error {
return nil
}
func updateIfNotZero[T comparable](target *T, source T) {
var zero T
if source != zero {
*target = source
}
}
// NewLinkSpec initializes a LinkSpec resource.
func NewLinkSpec(namespace resource.Namespace, id resource.ID) *LinkSpec {
return typed.NewResource[LinkSpecSpec, LinkSpecRD](

View File

@ -26,7 +26,10 @@ func TestLinkSpecMarshalYAML(t *testing.T) {
Kind: "eth",
Type: nethelpers.LinkEther,
ParentName: "eth1",
MasterName: "bond0",
BondSlave: network.BondSlave{
MasterName: "bond0",
SlaveIndex: 0,
},
VLAN: network.VLANSpec{
VID: 25,
Protocol: nethelpers.VLANProtocol8021AD,