github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/scrape/discovery/kubernetes/kubernetes.go (about) 1 // Copyright 2016 The Prometheus Authors 2 // Copyright 2021 The Pyroscope Authors 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 package kubernetes 17 18 import ( 19 "context" 20 "fmt" 21 "reflect" 22 "regexp" 23 "strings" 24 "sync" 25 "time" 26 27 "github.com/prometheus/client_golang/prometheus" 28 "github.com/sirupsen/logrus" 29 apiv1 "k8s.io/api/core/v1" 30 disv1beta1 "k8s.io/api/discovery/v1beta1" 31 networkv1 "k8s.io/api/networking/v1" 32 "k8s.io/api/networking/v1beta1" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/fields" 35 "k8s.io/apimachinery/pkg/labels" 36 "k8s.io/apimachinery/pkg/runtime" 37 utilversion "k8s.io/apimachinery/pkg/util/version" 38 "k8s.io/apimachinery/pkg/watch" 39 k8s "k8s.io/client-go/kubernetes" 40 "k8s.io/client-go/rest" 41 "k8s.io/client-go/tools/cache" 42 "k8s.io/client-go/tools/clientcmd" 43 44 "github.com/pyroscope-io/pyroscope/pkg/build" 45 "github.com/pyroscope-io/pyroscope/pkg/scrape/config" 46 "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery" 47 "github.com/pyroscope-io/pyroscope/pkg/scrape/discovery/targetgroup" 48 "github.com/pyroscope-io/pyroscope/pkg/scrape/model" 49 ) 50 51 // revive:disable:max-public-structs complex domain 52 // revive:disable:cognitive-complexity preserve original implementation 53 54 const ( 55 // kubernetesMetaLabelPrefix is the meta prefix used for all meta labels. 56 // in this discovery. 57 metaLabelPrefix = model.MetaLabelPrefix + "kubernetes_" 58 namespaceLabel = metaLabelPrefix + "namespace" 59 metricsNamespace = "pyroscope_sd_kubernetes" 60 presentValue = model.LabelValue("true") 61 ) 62 63 var ( 64 // Http header 65 userAgent = fmt.Sprintf("Pyroscope/%s", build.Version) 66 // Custom events metric 67 eventCount = prometheus.NewCounterVec( 68 prometheus.CounterOpts{ 69 Namespace: metricsNamespace, 70 Name: "events_total", 71 Help: "The number of Kubernetes events handled.", 72 }, 73 []string{"role", "event"}, 74 ) 75 // DefaultSDConfig is the default Kubernetes SD configuration 76 DefaultSDConfig = SDConfig{ 77 HTTPClientConfig: config.DefaultHTTPClientConfig, 78 } 79 ) 80 81 func init() { 82 discovery.RegisterConfig(&SDConfig{}) 83 prometheus.MustRegister(eventCount) 84 // Initialize metric vectors. 85 for _, role := range []string{"endpointslice", "endpoints", "node", "pod", "service", "ingress"} { 86 for _, evt := range []string{"add", "delete", "update"} { 87 eventCount.WithLabelValues(role, evt) 88 } 89 } 90 (&clientGoRequestMetricAdapter{}).Register(prometheus.DefaultRegisterer) 91 (&clientGoWorkqueueMetricsProvider{}).Register(prometheus.DefaultRegisterer) 92 } 93 94 // Role is role of the service in Kubernetes. 95 type Role string 96 97 // The valid options for Role. 98 const ( 99 RoleNode Role = "node" 100 RolePod Role = "pod" 101 RoleService Role = "service" 102 RoleEndpoint Role = "endpoints" 103 RoleEndpointSlice Role = "endpointslice" 104 RoleIngress Role = "ingress" 105 ) 106 107 // UnmarshalYAML implements the yaml.Unmarshaler interface. 108 func (c *Role) UnmarshalYAML(unmarshal func(interface{}) error) error { 109 if err := unmarshal((*string)(c)); err != nil { 110 return err 111 } 112 switch *c { 113 case RoleNode, RolePod, RoleService, RoleEndpoint, RoleEndpointSlice, RoleIngress: 114 return nil 115 default: 116 return fmt.Errorf("unknown Kubernetes SD role %q", *c) 117 } 118 } 119 120 // SDConfig is the configuration for Kubernetes service discovery. 121 type SDConfig struct { 122 APIServer config.URL `yaml:"api-server,omitempty"` 123 Role Role `yaml:"role"` 124 KubeConfig string `yaml:"kubeconfig-file"` 125 HTTPClientConfig config.HTTPClientConfig `yaml:",inline"` 126 NamespaceDiscovery NamespaceDiscovery `yaml:"namespaces,omitempty"` 127 Selectors []SelectorConfig `yaml:"selectors,omitempty"` 128 } 129 130 // Name returns the name of the Config. 131 func (*SDConfig) Name() string { return "kubernetes" } 132 133 // NewDiscoverer returns a Discoverer for the Config. 134 func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) { 135 return New(opts.Logger, c) 136 } 137 138 // SetDirectory joins any relative file paths with dir. 139 func (c *SDConfig) SetDirectory(dir string) { 140 c.HTTPClientConfig.SetDirectory(dir) 141 c.KubeConfig = config.JoinDir(dir, c.KubeConfig) 142 } 143 144 type roleSelector struct { 145 node resourceSelector 146 pod resourceSelector 147 service resourceSelector 148 endpoints resourceSelector 149 endpointslice resourceSelector 150 ingress resourceSelector 151 } 152 153 type SelectorConfig struct { 154 Role Role `yaml:"role,omitempty"` 155 Label string `yaml:"label,omitempty"` 156 Field string `yaml:"field,omitempty"` 157 } 158 159 type resourceSelector struct { 160 label string 161 field string 162 } 163 164 // UnmarshalYAML implements the yaml.Unmarshaler interface. 165 func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 166 *c = DefaultSDConfig 167 type plain SDConfig 168 err := unmarshal((*plain)(c)) 169 if err != nil { 170 return err 171 } 172 if c.Role == "" { 173 return fmt.Errorf("role missing (one of: pod, service, endpoints, endpointslice, node, ingress)") 174 } 175 err = c.HTTPClientConfig.Validate() 176 if err != nil { 177 return err 178 } 179 if c.APIServer.URL != nil && c.KubeConfig != "" { 180 // Api-server and kubeconfig-file are mutually exclusive 181 return fmt.Errorf("cannot use 'kubeconfig-file' and 'api-server' simultaneously") 182 } 183 if c.KubeConfig != "" && !reflect.DeepEqual(c.HTTPClientConfig, config.DefaultHTTPClientConfig) { 184 // Kubeconfig-file and custom http config are mutually exclusive 185 return fmt.Errorf("cannot use a custom HTTP client configuration together with 'kubeconfig-file'") 186 } 187 if c.APIServer.URL == nil && !reflect.DeepEqual(c.HTTPClientConfig, config.DefaultHTTPClientConfig) { 188 return fmt.Errorf("to use custom HTTP client configuration please provide the 'api-server' URL explicitly") 189 } 190 191 foundSelectorRoles := make(map[Role]struct{}) 192 allowedSelectors := map[Role][]string{ 193 RolePod: {string(RolePod)}, 194 RoleService: {string(RoleService)}, 195 RoleEndpointSlice: {string(RolePod), string(RoleService), string(RoleEndpointSlice)}, 196 RoleEndpoint: {string(RolePod), string(RoleService), string(RoleEndpoint)}, 197 RoleNode: {string(RoleNode)}, 198 RoleIngress: {string(RoleIngress)}, 199 } 200 201 for _, selector := range c.Selectors { 202 if _, ok := foundSelectorRoles[selector.Role]; ok { 203 return fmt.Errorf("duplicated selector role: %s", selector.Role) 204 } 205 foundSelectorRoles[selector.Role] = struct{}{} 206 207 if _, ok := allowedSelectors[c.Role]; !ok { 208 return fmt.Errorf("invalid role: %q, expecting one of: pod, service, endpoints, endpointslice, node or ingress", c.Role) 209 } 210 var allowed bool 211 for _, role := range allowedSelectors[c.Role] { 212 if role == string(selector.Role) { 213 allowed = true 214 break 215 } 216 } 217 218 if !allowed { 219 return fmt.Errorf("%s role supports only %s selectors", c.Role, strings.Join(allowedSelectors[c.Role], ", ")) 220 } 221 222 _, err := fields.ParseSelector(selector.Field) 223 if err != nil { 224 return err 225 } 226 _, err = labels.Parse(selector.Label) 227 if err != nil { 228 return err 229 } 230 } 231 return nil 232 } 233 234 // NamespaceDiscovery is the configuration for discovering 235 // Kubernetes namespaces. 236 type NamespaceDiscovery struct { 237 Names []string `yaml:"names"` 238 } 239 240 // UnmarshalYAML implements the yaml.Unmarshaler interface. 241 func (c *NamespaceDiscovery) UnmarshalYAML(unmarshal func(interface{}) error) error { 242 *c = NamespaceDiscovery{} 243 type plain NamespaceDiscovery 244 return unmarshal((*plain)(c)) 245 } 246 247 // Discovery implements the discoverer interface for discovering 248 // targets from Kubernetes. 249 type Discovery struct { 250 sync.RWMutex 251 client k8s.Interface 252 role Role 253 logger logrus.FieldLogger 254 namespaceDiscovery *NamespaceDiscovery 255 discoverers []discovery.Discoverer 256 selectors roleSelector 257 } 258 259 func (d *Discovery) getNamespaces() []string { 260 namespaces := d.namespaceDiscovery.Names 261 if len(namespaces) == 0 { 262 namespaces = []string{apiv1.NamespaceAll} 263 } 264 return namespaces 265 } 266 267 // New creates a new Kubernetes discovery for the given role. 268 func New(l logrus.FieldLogger, conf *SDConfig) (*Discovery, error) { 269 var ( 270 kcfg *rest.Config 271 err error 272 ) 273 if conf.KubeConfig != "" { 274 kcfg, err = clientcmd.BuildConfigFromFlags("", conf.KubeConfig) 275 if err != nil { 276 return nil, err 277 } 278 } else if conf.APIServer.URL == nil { 279 // Use the Kubernetes provided pod service account 280 // as described in https://kubernetes.io/docs/admin/service-accounts-admin/ 281 kcfg, err = rest.InClusterConfig() 282 if err != nil { 283 return nil, err 284 } 285 l.Debug("using pod service account via in-cluster config") 286 } else { 287 rt, err := config.NewRoundTripperFromConfig(conf.HTTPClientConfig, "kubernetes-sd") 288 if err != nil { 289 return nil, err 290 } 291 kcfg = &rest.Config{ 292 Host: conf.APIServer.String(), 293 Transport: rt, 294 } 295 } 296 297 kcfg.UserAgent = userAgent 298 299 c, err := k8s.NewForConfig(kcfg) 300 if err != nil { 301 return nil, err 302 } 303 return &Discovery{ 304 client: c, 305 logger: l, 306 role: conf.Role, 307 namespaceDiscovery: &conf.NamespaceDiscovery, 308 discoverers: make([]discovery.Discoverer, 0), 309 selectors: mapSelector(conf.Selectors), 310 }, nil 311 } 312 313 func mapSelector(rawSelector []SelectorConfig) roleSelector { 314 rs := roleSelector{} 315 for _, resourceSelectorRaw := range rawSelector { 316 switch resourceSelectorRaw.Role { 317 case RoleEndpointSlice: 318 rs.endpointslice.field = resourceSelectorRaw.Field 319 rs.endpointslice.label = resourceSelectorRaw.Label 320 case RoleEndpoint: 321 rs.endpoints.field = resourceSelectorRaw.Field 322 rs.endpoints.label = resourceSelectorRaw.Label 323 case RoleIngress: 324 rs.ingress.field = resourceSelectorRaw.Field 325 rs.ingress.label = resourceSelectorRaw.Label 326 case RoleNode: 327 rs.node.field = resourceSelectorRaw.Field 328 rs.node.label = resourceSelectorRaw.Label 329 case RolePod: 330 rs.pod.field = resourceSelectorRaw.Field 331 rs.pod.label = resourceSelectorRaw.Label 332 case RoleService: 333 rs.service.field = resourceSelectorRaw.Field 334 rs.service.label = resourceSelectorRaw.Label 335 } 336 } 337 return rs 338 } 339 340 const resyncPeriod = 10 * time.Minute 341 342 // Run implements the discoverer interface. 343 func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) { 344 d.Lock() 345 namespaces := d.getNamespaces() 346 347 switch d.role { 348 case RoleEndpointSlice: 349 for _, namespace := range namespaces { 350 e := d.client.DiscoveryV1beta1().EndpointSlices(namespace) 351 elw := &cache.ListWatch{ 352 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 353 options.FieldSelector = d.selectors.endpointslice.field 354 options.LabelSelector = d.selectors.endpointslice.label 355 return e.List(ctx, options) 356 }, 357 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 358 options.FieldSelector = d.selectors.endpointslice.field 359 options.LabelSelector = d.selectors.endpointslice.label 360 return e.Watch(ctx, options) 361 }, 362 } 363 s := d.client.CoreV1().Services(namespace) 364 slw := &cache.ListWatch{ 365 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 366 options.FieldSelector = d.selectors.service.field 367 options.LabelSelector = d.selectors.service.label 368 return s.List(ctx, options) 369 }, 370 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 371 options.FieldSelector = d.selectors.service.field 372 options.LabelSelector = d.selectors.service.label 373 return s.Watch(ctx, options) 374 }, 375 } 376 p := d.client.CoreV1().Pods(namespace) 377 plw := &cache.ListWatch{ 378 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 379 options.FieldSelector = d.selectors.pod.field 380 options.LabelSelector = d.selectors.pod.label 381 return p.List(ctx, options) 382 }, 383 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 384 options.FieldSelector = d.selectors.pod.field 385 options.LabelSelector = d.selectors.pod.label 386 return p.Watch(ctx, options) 387 }, 388 } 389 eps := NewEndpointSlice( 390 d.logger.WithField("role", "endpointslice"), 391 cache.NewSharedInformer(slw, &apiv1.Service{}, resyncPeriod), 392 cache.NewSharedInformer(elw, &disv1beta1.EndpointSlice{}, resyncPeriod), 393 cache.NewSharedInformer(plw, &apiv1.Pod{}, resyncPeriod), 394 ) 395 d.discoverers = append(d.discoverers, eps) 396 go eps.endpointSliceInf.Run(ctx.Done()) 397 go eps.serviceInf.Run(ctx.Done()) 398 go eps.podInf.Run(ctx.Done()) 399 } 400 case RoleEndpoint: 401 for _, namespace := range namespaces { 402 e := d.client.CoreV1().Endpoints(namespace) 403 elw := &cache.ListWatch{ 404 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 405 options.FieldSelector = d.selectors.endpoints.field 406 options.LabelSelector = d.selectors.endpoints.label 407 return e.List(ctx, options) 408 }, 409 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 410 options.FieldSelector = d.selectors.endpoints.field 411 options.LabelSelector = d.selectors.endpoints.label 412 return e.Watch(ctx, options) 413 }, 414 } 415 s := d.client.CoreV1().Services(namespace) 416 slw := &cache.ListWatch{ 417 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 418 options.FieldSelector = d.selectors.service.field 419 options.LabelSelector = d.selectors.service.label 420 return s.List(ctx, options) 421 }, 422 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 423 options.FieldSelector = d.selectors.service.field 424 options.LabelSelector = d.selectors.service.label 425 return s.Watch(ctx, options) 426 }, 427 } 428 p := d.client.CoreV1().Pods(namespace) 429 plw := &cache.ListWatch{ 430 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 431 options.FieldSelector = d.selectors.pod.field 432 options.LabelSelector = d.selectors.pod.label 433 return p.List(ctx, options) 434 }, 435 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 436 options.FieldSelector = d.selectors.pod.field 437 options.LabelSelector = d.selectors.pod.label 438 return p.Watch(ctx, options) 439 }, 440 } 441 eps := NewEndpoints( 442 d.logger.WithField("role", "endpoint"), 443 cache.NewSharedInformer(slw, &apiv1.Service{}, resyncPeriod), 444 cache.NewSharedInformer(elw, &apiv1.Endpoints{}, resyncPeriod), 445 cache.NewSharedInformer(plw, &apiv1.Pod{}, resyncPeriod), 446 ) 447 d.discoverers = append(d.discoverers, eps) 448 go eps.endpointsInf.Run(ctx.Done()) 449 go eps.serviceInf.Run(ctx.Done()) 450 go eps.podInf.Run(ctx.Done()) 451 } 452 case RolePod: 453 for _, namespace := range namespaces { 454 p := d.client.CoreV1().Pods(namespace) 455 plw := &cache.ListWatch{ 456 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 457 options.FieldSelector = d.selectors.pod.field 458 options.LabelSelector = d.selectors.pod.label 459 return p.List(ctx, options) 460 }, 461 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 462 options.FieldSelector = d.selectors.pod.field 463 options.LabelSelector = d.selectors.pod.label 464 return p.Watch(ctx, options) 465 }, 466 } 467 pod := NewPod( 468 d.logger.WithField("role", "pod"), 469 cache.NewSharedInformer(plw, &apiv1.Pod{}, resyncPeriod), 470 ) 471 d.discoverers = append(d.discoverers, pod) 472 go pod.informer.Run(ctx.Done()) 473 } 474 case RoleService: 475 for _, namespace := range namespaces { 476 s := d.client.CoreV1().Services(namespace) 477 slw := &cache.ListWatch{ 478 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 479 options.FieldSelector = d.selectors.service.field 480 options.LabelSelector = d.selectors.service.label 481 return s.List(ctx, options) 482 }, 483 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 484 options.FieldSelector = d.selectors.service.field 485 options.LabelSelector = d.selectors.service.label 486 return s.Watch(ctx, options) 487 }, 488 } 489 svc := NewService( 490 d.logger.WithField("role", "service"), 491 cache.NewSharedInformer(slw, &apiv1.Service{}, resyncPeriod), 492 ) 493 d.discoverers = append(d.discoverers, svc) 494 go svc.informer.Run(ctx.Done()) 495 } 496 case RoleIngress: 497 // Check "networking.k8s.io/v1" availability with retries. 498 // If "v1" is not avaiable, use "networking.k8s.io/v1beta1" for backward compatibility 499 var v1Supported bool 500 if retryOnError(ctx, 10*time.Second, 501 func() (err error) { 502 v1Supported, err = checkNetworkingV1Supported(d.client) 503 if err != nil { 504 d.logger.WithError(err).Error("failed to check networking.k8s.io/v1 availability") 505 } 506 return err 507 }, 508 ) { 509 d.Unlock() 510 return 511 } 512 513 for _, namespace := range namespaces { 514 var informer cache.SharedInformer 515 if v1Supported { 516 i := d.client.NetworkingV1().Ingresses(namespace) 517 ilw := &cache.ListWatch{ 518 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 519 options.FieldSelector = d.selectors.ingress.field 520 options.LabelSelector = d.selectors.ingress.label 521 return i.List(ctx, options) 522 }, 523 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 524 options.FieldSelector = d.selectors.ingress.field 525 options.LabelSelector = d.selectors.ingress.label 526 return i.Watch(ctx, options) 527 }, 528 } 529 informer = cache.NewSharedInformer(ilw, &networkv1.Ingress{}, resyncPeriod) 530 } else { 531 i := d.client.NetworkingV1beta1().Ingresses(namespace) 532 ilw := &cache.ListWatch{ 533 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 534 options.FieldSelector = d.selectors.ingress.field 535 options.LabelSelector = d.selectors.ingress.label 536 return i.List(ctx, options) 537 }, 538 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 539 options.FieldSelector = d.selectors.ingress.field 540 options.LabelSelector = d.selectors.ingress.label 541 return i.Watch(ctx, options) 542 }, 543 } 544 informer = cache.NewSharedInformer(ilw, &v1beta1.Ingress{}, resyncPeriod) 545 } 546 ingress := NewIngress( 547 d.logger.WithField("role", "ingress"), 548 informer, 549 ) 550 d.discoverers = append(d.discoverers, ingress) 551 go ingress.informer.Run(ctx.Done()) 552 } 553 case RoleNode: 554 nlw := &cache.ListWatch{ 555 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 556 options.FieldSelector = d.selectors.node.field 557 options.LabelSelector = d.selectors.node.label 558 return d.client.CoreV1().Nodes().List(ctx, options) 559 }, 560 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 561 options.FieldSelector = d.selectors.node.field 562 options.LabelSelector = d.selectors.node.label 563 return d.client.CoreV1().Nodes().Watch(ctx, options) 564 }, 565 } 566 node := NewNode( 567 d.logger.WithField("role", "node"), 568 cache.NewSharedInformer(nlw, &apiv1.Node{}, resyncPeriod), 569 ) 570 d.discoverers = append(d.discoverers, node) 571 go node.informer.Run(ctx.Done()) 572 default: 573 d.logger.WithField("role", d.role).Error("unknown Kubernetes discovery kind") 574 } 575 576 var wg sync.WaitGroup 577 for _, dd := range d.discoverers { 578 wg.Add(1) 579 go func(d discovery.Discoverer) { 580 defer wg.Done() 581 d.Run(ctx, ch) 582 }(dd) 583 } 584 585 d.Unlock() 586 587 wg.Wait() 588 <-ctx.Done() 589 } 590 591 func lv(s string) model.LabelValue { 592 return model.LabelValue(s) 593 } 594 595 func send(ctx context.Context, ch chan<- []*targetgroup.Group, tg *targetgroup.Group) { 596 if tg == nil { 597 return 598 } 599 select { 600 case <-ctx.Done(): 601 case ch <- []*targetgroup.Group{tg}: 602 } 603 } 604 605 func retryOnError(ctx context.Context, interval time.Duration, f func() error) (canceled bool) { 606 var err error 607 err = f() 608 for { 609 if err == nil { 610 return false 611 } 612 select { 613 case <-ctx.Done(): 614 return true 615 case <-time.After(interval): 616 err = f() 617 } 618 } 619 } 620 621 func checkNetworkingV1Supported(client k8s.Interface) (bool, error) { 622 k8sVer, err := client.Discovery().ServerVersion() 623 if err != nil { 624 return false, err 625 } 626 semVer, err := utilversion.ParseSemantic(k8sVer.String()) 627 if err != nil { 628 return false, err 629 } 630 // networking.k8s.io/v1 is available since Kubernetes v1.19 631 // https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.19.md 632 return semVer.Major() >= 1 && semVer.Minor() >= 19, nil 633 } 634 635 var invalidLabelCharRE = regexp.MustCompile(`[^a-zA-Z0-9_]`) 636 637 // sanitizeLabelName replaces anything that doesn't match 638 // client_label.LabelNameRE with an underscore. 639 func sanitizeLabelName(name string) string { 640 return invalidLabelCharRE.ReplaceAllString(name, "_") 641 }