|  | 
|  | 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 | +} | 
0 commit comments