github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/k8s_config.go (about) 1 package client 2 3 import ( 4 "context" 5 "encoding/csv" 6 "encoding/json" 7 "fmt" 8 "os" 9 "strings" 10 11 "github.com/spf13/pflag" 12 "gopkg.in/yaml.v3" 13 "k8s.io/apimachinery/pkg/api/meta" 14 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 "k8s.io/apimachinery/pkg/runtime" 16 "k8s.io/cli-runtime/pkg/genericclioptions" 17 "k8s.io/client-go/discovery" 18 "k8s.io/client-go/discovery/cached/memory" 19 _ "k8s.io/client-go/plugin/pkg/client/auth" // Important for various cloud provider auth 20 "k8s.io/client-go/rest" 21 "k8s.io/client-go/restmapper" 22 "k8s.io/client-go/tools/clientcmd" 23 "k8s.io/client-go/tools/clientcmd/api" 24 25 "github.com/datawire/dlib/dlog" 26 "github.com/telepresenceio/telepresence/rpc/v2/connector" 27 rpc "github.com/telepresenceio/telepresence/rpc/v2/daemon" 28 "github.com/telepresenceio/telepresence/v2/pkg/errcat" 29 "github.com/telepresenceio/telepresence/v2/pkg/iputil" 30 "github.com/telepresenceio/telepresence/v2/pkg/maps" 31 "github.com/telepresenceio/telepresence/v2/pkg/proc" 32 ) 33 34 // DNSMapping contains a hostname and its associated alias. When requesting the name, the intended behavior is 35 // to resolve the alias instead. 36 type DNSMapping struct { 37 Name string `json:"name,omitempty" yaml:"name,omitempty"` 38 AliasFor string `json:"aliasFor,omitempty" yaml:"aliasFor,omitempty"` 39 } 40 41 type DNSMappings []*DNSMapping 42 43 func (d *DNSMappings) FromRPC(rpcMappings []*rpc.DNSMapping) { 44 *d = make(DNSMappings, 0, len(rpcMappings)) 45 for i := range rpcMappings { 46 *d = append(*d, &DNSMapping{ 47 Name: rpcMappings[i].Name, 48 AliasFor: rpcMappings[i].AliasFor, 49 }) 50 } 51 } 52 53 func (d DNSMappings) ToRPC() []*rpc.DNSMapping { 54 rpcMappings := make([]*rpc.DNSMapping, 0, len(d)) 55 for i := range d { 56 rpcMappings = append(rpcMappings, &rpc.DNSMapping{ 57 Name: d[i].Name, 58 AliasFor: d[i].AliasFor, 59 }) 60 } 61 return rpcMappings 62 } 63 64 // The DnsConfig is part of the KubeconfigExtension struct. 65 type DnsConfig struct { 66 // LocalIP is the address of the local DNS server. This entry is only 67 // used on Linux system that are not configured to use systemd-resolved and 68 // can be overridden by using the option --dns on the command line and defaults 69 // to the first line of /etc/resolv.conf 70 LocalIP iputil.IPKey `json:"local-ip,omitempty"` 71 72 // RemoteIP is the address of the cluster's DNS service. It will default 73 // to the IP of the kube-dns.kube-system or the dns-default.openshift-dns service. 74 RemoteIP iputil.IPKey `json:"remote-ip,omitempty"` 75 76 // ExcludeSuffixes are suffixes for which the DNS resolver will always return 77 // NXDOMAIN (or fallback in case of the overriding resolver). 78 ExcludeSuffixes []string `json:"exclude-suffixes,omitempty"` 79 80 // IncludeSuffixes are suffixes for which the DNS resolver will always attempt to do 81 // a lookup. Includes have higher priority than excludes. 82 IncludeSuffixes []string `json:"include-suffixes,omitempty"` 83 84 // Excludes are a list of hostname that the DNS resolver will not resolve even if they exist. 85 Excludes []string `json:"excludes,omitempty"` 86 87 // Mappings contains a list of DNS Mappings. Each item references a hostname, and an associated alias. If a 88 // request is made for the name, the alias will be resolved instead. 89 Mappings DNSMappings `json:"mappings,omitempty"` 90 91 // The maximum time to wait for a cluster side host lookup. 92 LookupTimeout v1.Duration `json:"lookup-timeout,omitempty"` 93 } 94 95 // The ManagerConfig is part of the KubeconfigExtension struct. It configures discovery of the traffic manager. 96 type ManagerConfig struct { 97 // Namespace is the name of the namespace where the traffic manager is to be found 98 Namespace string `json:"namespace,omitempty"` 99 } 100 101 // KubeconfigExtension is an extension read from the selected kubeconfig Cluster. 102 type KubeconfigExtension struct { 103 DNS *DnsConfig `json:"dns,omitempty"` 104 AlsoProxy []*iputil.Subnet `json:"also-proxy,omitempty"` 105 NeverProxy []*iputil.Subnet `json:"never-proxy,omitempty"` 106 AllowConflictingSubnets []*iputil.Subnet `json:"allow-conflicting-subnets,omitempty"` 107 Manager *ManagerConfig `json:"manager,omitempty"` 108 } 109 110 // Kubeconfig implements genericclioptions.RESTClientGetter, but is using the RestConfig 111 // instead of the ConfigFlags (which also implements that interface) since the latter 112 // will assume that the kubeconfig is loaded from disk. 113 type Kubeconfig struct { 114 KubeconfigExtension 115 Namespace string // default cluster namespace. 116 Context string 117 Server string 118 OriginalFlagMap map[string]string 119 EffectiveFlagMap map[string]string 120 ClientConfig clientcmd.ClientConfig 121 RestConfig *rest.Config 122 } 123 124 func (kf *Kubeconfig) ToRESTConfig() (*rest.Config, error) { 125 return kf.RestConfig, nil 126 } 127 128 func (kf *Kubeconfig) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { 129 discoveryClient, err := discovery.NewDiscoveryClientForConfig(kf.RestConfig) 130 if err != nil { 131 return nil, err 132 } 133 return memory.NewMemCacheClient(discoveryClient), nil 134 } 135 136 func (kf *Kubeconfig) ToRESTMapper() (meta.RESTMapper, error) { 137 discoveryClient, err := kf.ToDiscoveryClient() 138 if err != nil { 139 return nil, err 140 } 141 mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) 142 expander := restmapper.NewShortcutExpander(mapper, discoveryClient, func(string) {}) 143 return expander, nil 144 } 145 146 func (kf *Kubeconfig) ToRawKubeConfigLoader() clientcmd.ClientConfig { 147 return kf.ClientConfig 148 } 149 150 const configExtension = "telepresence.io" 151 152 func ConfigFlags(flagMap map[string]string) (*genericclioptions.ConfigFlags, error) { 153 configFlags := genericclioptions.NewConfigFlags(false) 154 flags := pflag.NewFlagSet("", 0) 155 configFlags.AddFlags(flags) 156 for k, v := range flagMap { 157 f := flags.Lookup(k) 158 if f == nil { 159 continue 160 } 161 var err error 162 if sv, ok := f.Value.(pflag.SliceValue); ok { 163 var vs []string 164 if vs, err = csv.NewReader(strings.NewReader(v)).Read(); err == nil { 165 err = sv.Replace(vs) 166 } 167 } else { 168 err = flags.Set(k, v) 169 } 170 if err != nil { 171 return nil, errcat.User.Newf("error processing kubectl flag --%s=%s: %w", k, v, err) 172 } 173 } 174 return configFlags, nil 175 } 176 177 // ConfigLoader returns the name of the current Kubernetes context, and the context itself. 178 func ConfigLoader(ctx context.Context, flagMap map[string]string, kubeConfigData []byte) (clientcmd.ClientConfig, error) { 179 configFlags, err := ConfigFlags(flagMap) 180 if err != nil { 181 return nil, err 182 } 183 return NewClientConfig(ctx, configFlags, kubeConfigData) 184 } 185 186 // CurrentContext returns the name of the current Kubernetes context, the active namespace, and the context itself. 187 func CurrentContext(ctx context.Context, flagMap map[string]string, configBytes []byte) (string, string, *api.Context, error) { 188 cld, err := ConfigLoader(ctx, flagMap, configBytes) 189 if err != nil { 190 return "", "", nil, err 191 } 192 ns, _, err := cld.Namespace() 193 if err != nil { 194 return "", "", nil, err 195 } 196 197 config, err := cld.RawConfig() 198 if err != nil { 199 return "", "", nil, err 200 } 201 if len(config.Contexts) == 0 { 202 return "", "", nil, errcat.Config.New("kubeconfig has no context definition") 203 } 204 cc := flagMap["context"] 205 if cc == "" { 206 cc = config.CurrentContext 207 } 208 return cc, ns, config.Contexts[cc], nil 209 } 210 211 func NewKubeconfig(c context.Context, flagMap map[string]string, managerNamespaceOverride string) (*Kubeconfig, error) { 212 configFlags, err := ConfigFlags(flagMap) 213 if err != nil { 214 return nil, err 215 } 216 return newKubeconfig(c, flagMap, flagMap, managerNamespaceOverride, configFlags, nil) 217 } 218 219 func DaemonKubeconfig(c context.Context, cr *connector.ConnectRequest) (*Kubeconfig, error) { 220 if cr.IsPodDaemon { 221 return NewInClusterConfig(c, cr.KubeFlags) 222 } 223 flagMap := cr.KubeFlags 224 if proc.RunningInContainer() { 225 // Don't trust the host's KUBECONFIG env. 226 delete(cr.Environment, "KUBECONFIG") 227 228 // Add potential overrides for kube flags. 229 if len(cr.ContainerKubeFlagOverrides) > 0 { 230 flagMap = maps.Copy(flagMap) 231 maps.Merge(flagMap, cr.ContainerKubeFlagOverrides) 232 } 233 } 234 for k, v := range cr.Environment { 235 if k[0] == '-' { 236 _ = os.Unsetenv(k[1:]) 237 } else { 238 _ = os.Setenv(k, v) 239 } 240 } 241 configFlags, err := ConfigFlags(flagMap) 242 if err != nil { 243 return nil, err 244 } 245 return newKubeconfig(c, cr.KubeFlags, flagMap, cr.ManagerNamespace, configFlags, cr.KubeconfigData) 246 } 247 248 // AppendKubeFlags appends the flags in the given map to the given slice in the form of 249 // flag arguments suitable for command execution. Flags known to be multivalued are assumed 250 // to be in the form of comma-separated list and will be added using repeated options. 251 func AppendKubeFlags(kubeFlags map[string]string, args []string) ([]string, error) { 252 for k, v := range kubeFlags { 253 switch k { 254 case "as-group": 255 // Multivalued 256 r := csv.NewReader(strings.NewReader(v)) 257 gs, err := r.Read() 258 if err != nil { 259 return nil, err 260 } 261 for _, g := range gs { 262 args = append(args, "--"+k, g) 263 } 264 case "disable-compression", "insecure-skip-tls-verify": 265 // Boolean with false default. 266 if v != "false" { 267 args = append(args, "--"+k) 268 } 269 default: 270 args = append(args, "--"+k, v) 271 } 272 } 273 return args, nil 274 } 275 276 // flagOverrides creates overrides based on the given ConfigFlags. 277 // 278 // The code in this function is copied from clientcmd.config_flags.go, function toRawKubeConfigLoader 279 // but differs in that overrides are only made for non-zero values. 280 func flagOverrides(f *genericclioptions.ConfigFlags) *clientcmd.ConfigOverrides { 281 overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults} 282 283 stringVal := func(vp *string) (v string, ok bool) { 284 if vp != nil && *vp != "" { 285 v, ok = *vp, true 286 } 287 return 288 } 289 290 // bind auth info flag values to overrides 291 if v, ok := stringVal(f.CertFile); ok { 292 overrides.AuthInfo.ClientCertificate = v 293 } 294 if v, ok := stringVal(f.KeyFile); ok { 295 overrides.AuthInfo.ClientKey = v 296 } 297 if v, ok := stringVal(f.BearerToken); ok { 298 overrides.AuthInfo.Token = v 299 } 300 if v, ok := stringVal(f.Impersonate); ok { 301 overrides.AuthInfo.Impersonate = v 302 } 303 if v, ok := stringVal(f.ImpersonateUID); ok { 304 overrides.AuthInfo.ImpersonateUID = v 305 } 306 if f.ImpersonateGroup != nil && len(*f.ImpersonateGroup) > 0 { 307 overrides.AuthInfo.ImpersonateGroups = *f.ImpersonateGroup 308 } 309 if v, ok := stringVal(f.Username); ok { 310 overrides.AuthInfo.Username = v 311 } 312 if v, ok := stringVal(f.Password); ok { 313 overrides.AuthInfo.Password = v 314 } 315 316 // bind cluster flags 317 if v, ok := stringVal(f.APIServer); ok { 318 overrides.ClusterInfo.Server = v 319 } 320 if v, ok := stringVal(f.TLSServerName); ok { 321 overrides.ClusterInfo.TLSServerName = v 322 } 323 if v, ok := stringVal(f.CAFile); ok { 324 overrides.ClusterInfo.CertificateAuthority = v 325 } 326 if f.Insecure != nil && *f.Insecure { 327 overrides.ClusterInfo.InsecureSkipTLSVerify = true 328 } 329 if f.DisableCompression != nil && *f.DisableCompression { 330 overrides.ClusterInfo.DisableCompression = true 331 } 332 333 // bind context flags 334 if v, ok := stringVal(f.Context); ok { 335 overrides.CurrentContext = v 336 } 337 if v, ok := stringVal(f.ClusterName); ok { 338 overrides.Context.Cluster = v 339 } 340 if v, ok := stringVal(f.AuthInfoName); ok { 341 overrides.Context.AuthInfo = v 342 } 343 if v, ok := stringVal(f.Namespace); ok { 344 overrides.Context.Namespace = v 345 } 346 347 if v, ok := stringVal(f.Timeout); ok && v != "0" { 348 overrides.Timeout = v 349 } 350 return overrides 351 } 352 353 type KubeconfigGetter func() (*api.Config, error) 354 355 type configGetter struct { 356 kubeconfigGetter KubeconfigGetter 357 destFile string 358 } 359 360 func (g *configGetter) Load() (*api.Config, error) { 361 return g.kubeconfigGetter() 362 } 363 364 func (g *configGetter) GetLoadingPrecedence() []string { 365 return nil 366 } 367 368 func (g *configGetter) GetStartingConfig() (*api.Config, error) { 369 return g.kubeconfigGetter() 370 } 371 372 func (g *configGetter) GetDefaultFilename() string { 373 if g.destFile == "" { 374 destFile, err := os.CreateTemp("", "kc-*") 375 if err == nil { 376 g.destFile = destFile.Name() 377 _ = os.Remove(destFile.Name()) 378 destFile.Close() 379 } 380 } 381 return g.destFile 382 } 383 384 func (g *configGetter) IsExplicitFile() bool { 385 return false 386 } 387 388 func (g *configGetter) GetExplicitFile() string { 389 return "" 390 } 391 392 func (g *configGetter) IsDefaultConfig(config *rest.Config) bool { 393 return false 394 } 395 396 // NewClientConfig creates a clientcmd.ClientConfig, by either reading the kubeconfig from the given configData or 397 // by loading it from files as configured by the given configFlags. 398 func NewClientConfig(ctx context.Context, configFlags *genericclioptions.ConfigFlags, configData []byte) (clientcmd.ClientConfig, error) { 399 if len(configData) == 0 { 400 return configFlags.ToRawKubeConfigLoader(), nil 401 } 402 directConfig, err := clientcmd.NewClientConfigFromBytes(configData) 403 if err != nil { 404 dlog.Errorf(ctx, "loading kubeconfig failed: %v", err) 405 return nil, err 406 } 407 config, err := directConfig.RawConfig() 408 if err != nil { 409 dlog.Errorf(ctx, "raw kubeconfig failed: %v", err) 410 return nil, err 411 } 412 overrides := flagOverrides(configFlags) 413 currentContext := overrides.CurrentContext 414 return clientcmd.NewNonInteractiveClientConfig(config, currentContext, overrides, &configGetter{ 415 kubeconfigGetter: func() (*api.Config, error) { 416 return &config, nil 417 }, 418 }), nil 419 } 420 421 func newKubeconfig( 422 ctx context.Context, 423 originalFlags, 424 effectiveFlags map[string]string, 425 managerNamespaceOverride string, 426 configFlags *genericclioptions.ConfigFlags, 427 configData []byte, 428 ) (*Kubeconfig, error) { 429 clientConfig, err := NewClientConfig(ctx, configFlags, configData) 430 if err != nil { 431 return nil, err 432 } 433 434 config, err := clientConfig.RawConfig() 435 if err != nil { 436 return nil, err 437 } 438 if len(config.Contexts) == 0 { 439 return nil, errcat.Config.New("kubeconfig has no context definition") 440 } 441 442 namespace, _, err := clientConfig.Namespace() 443 if err != nil { 444 return nil, err 445 } 446 447 ctxName := effectiveFlags["context"] 448 if ctxName == "" { 449 ctxName = config.CurrentContext 450 } 451 452 kubeCtx, ok := config.Contexts[ctxName] 453 if !ok { 454 return nil, errcat.Config.Newf("context %q does not exist in the kubeconfig", ctxName) 455 } 456 457 cluster, ok := config.Clusters[kubeCtx.Cluster] 458 if !ok { 459 return nil, errcat.Config.Newf("the cluster %q declared in context %q does exists in the kubeconfig", kubeCtx.Cluster, ctxName) 460 } 461 462 restConfig, err := clientConfig.ClientConfig() 463 if err != nil { 464 return nil, err 465 } 466 467 dlog.Debugf(ctx, "using namespace %q", namespace) 468 469 k := &Kubeconfig{ 470 Context: ctxName, 471 Server: cluster.Server, 472 Namespace: namespace, 473 EffectiveFlagMap: effectiveFlags, 474 OriginalFlagMap: originalFlags, 475 ClientConfig: clientConfig, 476 RestConfig: restConfig, 477 } 478 479 if ext, ok := cluster.Extensions[configExtension].(*runtime.Unknown); ok { 480 if err = json.Unmarshal(ext.Raw, &k.KubeconfigExtension); err != nil { 481 return nil, errcat.Config.Newf("unable to parse extension %s in kubeconfig: %w", configExtension, err) 482 } 483 } 484 485 if k.KubeconfigExtension.Manager == nil { 486 k.KubeconfigExtension.Manager = &ManagerConfig{} 487 } 488 489 if managerNamespaceOverride != "" { 490 k.KubeconfigExtension.Manager.Namespace = managerNamespaceOverride 491 } 492 493 if k.KubeconfigExtension.Manager.Namespace == "" { 494 k.KubeconfigExtension.Manager.Namespace = GetEnv(ctx).ManagerNamespace 495 } 496 if k.KubeconfigExtension.Manager.Namespace == "" { 497 k.KubeconfigExtension.Manager.Namespace = GetConfig(ctx).Cluster().DefaultManagerNamespace 498 } 499 return k, nil 500 } 501 502 // NewInClusterConfig represents an inClusterConfig. 503 func NewInClusterConfig(c context.Context, flagMap map[string]string) (*Kubeconfig, error) { 504 configFlags := genericclioptions.NewConfigFlags(false) 505 flags := pflag.NewFlagSet("", 0) 506 configFlags.AddFlags(flags) 507 for k, v := range flagMap { 508 if err := flags.Set(k, v); err != nil { 509 return nil, errcat.User.Newf("error processing kubectl flag --%s=%s: %w", k, v, err) 510 } 511 } 512 513 configLoader := configFlags.ToRawKubeConfigLoader() 514 restConfig, err := configLoader.ClientConfig() 515 if err != nil { 516 return nil, err 517 } 518 519 namespace, _, err := configLoader.Namespace() 520 if err != nil { 521 return nil, err 522 } 523 524 managerNamespace := GetEnv(c).ManagerNamespace 525 if managerNamespace == "" { 526 managerNamespace = GetConfig(c).Cluster().DefaultManagerNamespace 527 } 528 529 return &Kubeconfig{ 530 Namespace: namespace, 531 Server: restConfig.Host, 532 EffectiveFlagMap: flagMap, 533 OriginalFlagMap: flagMap, 534 RestConfig: restConfig, 535 ClientConfig: configLoader, 536 // it may be empty, but we should avoid nil deref 537 KubeconfigExtension: KubeconfigExtension{ 538 Manager: &ManagerConfig{ 539 Namespace: managerNamespace, 540 }, 541 }, 542 }, nil 543 } 544 545 // ContextServiceAndFlagsEqual determines if this instance is equal to the given instance with respect to context, 546 // server, and flag arguments. 547 func (kf *Kubeconfig) ContextServiceAndFlagsEqual(okf *Kubeconfig) bool { 548 return kf != nil && okf != nil && 549 kf.Context == okf.Context && 550 kf.Server == okf.Server && 551 maps.Equal(kf.EffectiveFlagMap, okf.EffectiveFlagMap) 552 } 553 554 func (kf *Kubeconfig) GetContext() string { 555 return kf.Context 556 } 557 558 func (kf *Kubeconfig) GetManagerNamespace() string { 559 return kf.KubeconfigExtension.Manager.Namespace 560 } 561 562 func (kf *Kubeconfig) GetRestConfig() *rest.Config { 563 return kf.RestConfig 564 } 565 566 func (kf *Kubeconfig) AddRemoteKubeConfigExtension(ctx context.Context, cfgYaml []byte) error { 567 remote := struct { 568 DNS *DNS `yaml:"dns,omitempty"` 569 Routing *Routing `yaml:"routing,omitempty"` 570 }{} 571 if err := yaml.Unmarshal(cfgYaml, &remote); err != nil { 572 return fmt.Errorf("unable to parse remote kubeconfig: %w", err) 573 } 574 if kf.DNS == nil { 575 kf.DNS = &DnsConfig{} 576 } 577 if dns := remote.DNS; dns != nil { 578 if kf.DNS.LocalIP == "" { 579 kf.DNS.LocalIP = iputil.IPKey(dns.LocalIP) 580 dlog.Debugf(ctx, "Applying remote dns local IP: %s", dns.LocalIP) 581 } 582 if kf.DNS.RemoteIP == "" { 583 kf.DNS.RemoteIP = iputil.IPKey(dns.RemoteIP) 584 dlog.Debugf(ctx, "Applying remote dns remote IP: %s", dns.RemoteIP) 585 } 586 if len(dns.ExcludeSuffixes) > 0 { 587 dlog.Debugf(ctx, "Applying remote excludeSuffixes: %v", dns.ExcludeSuffixes) 588 kf.DNS.ExcludeSuffixes = append(kf.DNS.ExcludeSuffixes, dns.ExcludeSuffixes...) 589 } 590 if len(dns.IncludeSuffixes) > 0 { 591 dlog.Debugf(ctx, "Applying remote includeSuffixes: %v", dns.IncludeSuffixes) 592 kf.DNS.IncludeSuffixes = append(kf.DNS.IncludeSuffixes, dns.IncludeSuffixes...) 593 } 594 if len(dns.Excludes) > 0 { 595 dlog.Debugf(ctx, "Applying remote excludes: %v", dns.Excludes) 596 kf.DNS.Excludes = append(kf.DNS.Excludes, dns.Excludes...) 597 } 598 if len(dns.Mappings) > 0 { 599 for _, m := range dns.Mappings { 600 dlog.Debugf(ctx, "Applying remote mapping: Name: %s, AliasFor %s", m.Name, m.AliasFor) 601 } 602 kf.DNS.Mappings = append(kf.DNS.Mappings, dns.Mappings...) 603 } 604 605 if kf.DNS.LookupTimeout.Duration == 0 { 606 dlog.Debugf(ctx, "Applying remote lookupTimeout: %s", dns.LookupTimeout) 607 kf.DNS.LookupTimeout.Duration = dns.LookupTimeout 608 } 609 } 610 if routing := remote.Routing; routing != nil { 611 if len(routing.AlsoProxy) > 0 { 612 dlog.Debugf(ctx, "Applying remote alsoProxy: %v", routing.AlsoProxy) 613 kf.AlsoProxy = append(kf.AlsoProxy, routing.AlsoProxy...) 614 } 615 if len(routing.NeverProxy) > 0 { 616 dlog.Debugf(ctx, "Applying remote neverProxy: %v", routing.NeverProxy) 617 kf.NeverProxy = append(kf.NeverProxy, routing.NeverProxy...) 618 } 619 if len(routing.AllowConflicting) > 0 { 620 dlog.Debugf(ctx, "Applying remote allowConflicting: %v", routing.AllowConflicting) 621 kf.AllowConflictingSubnets = append(kf.AllowConflictingSubnets, routing.AllowConflicting...) 622 } 623 } 624 return nil 625 }