github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/kube/services/services.go (about) 1 package services 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "strconv" 8 "strings" 9 "time" 10 11 "github.com/jenkins-x/jx-logging/pkg/log" 12 "github.com/olli-ai/jx/v2/pkg/kube" 13 14 "github.com/olli-ai/jx/v2/pkg/util" 15 "github.com/pkg/errors" 16 v1 "k8s.io/api/core/v1" 17 "k8s.io/api/extensions/v1beta1" 18 meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/apimachinery/pkg/fields" 20 "k8s.io/apimachinery/pkg/util/wait" 21 "k8s.io/apimachinery/pkg/watch" 22 "k8s.io/client-go/kubernetes" 23 tools_watch "k8s.io/client-go/tools/watch" 24 ) 25 26 const ( 27 ExposeAnnotation = "fabric8.io/expose" 28 ExposeURLAnnotation = "fabric8.io/exposeUrl" 29 ExposeGeneratedByAnnotation = "fabric8.io/generated-by" 30 ExposeIngressName = "fabric8.io/ingress.name" 31 JenkinsXSkipTLSAnnotation = "jenkins-x.io/skip.tls" 32 ExposeIngressAnnotation = "fabric8.io/ingress.annotations" 33 CertManagerAnnotation = "certmanager.k8s.io/issuer" 34 CertManagerClusterAnnotation = "certmanager.k8s.io/cluster-issuer" 35 ServiceAppLabel = "app" 36 ) 37 38 type ServiceURL struct { 39 Name string 40 URL string 41 } 42 43 func GetServices(client kubernetes.Interface, ns string) (map[string]*v1.Service, error) { 44 answer := map[string]*v1.Service{} 45 list, err := client.CoreV1().Services(ns).List(meta_v1.ListOptions{}) 46 if err != nil { 47 return answer, fmt.Errorf("failed to load Services %s", err) 48 } 49 for _, r := range list.Items { 50 name := r.Name 51 copy := r 52 answer[name] = © 53 } 54 return answer, nil 55 } 56 57 // GetServicesByName returns a list of Service objects from a list of service names 58 func GetServicesByName(client kubernetes.Interface, ns string, services []string) ([]*v1.Service, error) { 59 answer := make([]*v1.Service, 0) 60 svcList, err := client.CoreV1().Services(ns).List(meta_v1.ListOptions{}) 61 if err != nil { 62 return answer, errors.Wrapf(err, "listing the services in namespace %q", ns) 63 } 64 for _, s := range svcList.Items { 65 i := util.StringArrayIndex(services, s.GetName()) 66 if i > 0 { 67 copy := s 68 answer = append(answer, ©) 69 } 70 } 71 return answer, nil 72 } 73 74 func GetServiceNames(client kubernetes.Interface, ns string, filter string) ([]string, error) { 75 names := []string{} 76 list, err := client.CoreV1().Services(ns).List(meta_v1.ListOptions{}) 77 if err != nil { 78 return names, fmt.Errorf("failed to load Services %s", err) 79 } 80 for _, r := range list.Items { 81 name := r.Name 82 if filter == "" || strings.Contains(name, filter) { 83 names = append(names, name) 84 } 85 } 86 sort.Strings(names) 87 return names, nil 88 } 89 90 func GetServiceURLFromMap(services map[string]*v1.Service, name string) string { 91 return GetServiceURL(services[name]) 92 } 93 94 func FindServiceURL(client kubernetes.Interface, namespace string, name string) (string, error) { 95 log.Logger().Debugf("Finding service url for %s in namespace %s", name, namespace) 96 svc, err := client.CoreV1().Services(namespace).Get(name, meta_v1.GetOptions{}) 97 if err != nil { 98 return "", errors.Wrapf(err, "finding the service %s in namespace %s", name, namespace) 99 } 100 answer := GetServiceURL(svc) 101 if answer != "" { 102 log.Logger().Debugf("Found service url %s", answer) 103 return answer, nil 104 } 105 106 log.Logger().Debugf("Couldn't find service url, attempting to look up via ingress") 107 108 // lets try find the service via Ingress 109 ing, err := client.ExtensionsV1beta1().Ingresses(namespace).Get(name, meta_v1.GetOptions{}) 110 if err != nil { 111 log.Logger().Debugf("Unable to finding ingress for %s in namespace %s - err %s", name, namespace, err) 112 return "", errors.Wrapf(err, "getting ingress for service %q in namespace %s", name, namespace) 113 } 114 115 url := IngressURL(ing) 116 if url == "" { 117 log.Logger().Debugf("Unable to find service url via ingress for %s in namespace %s", name, namespace) 118 } 119 return url, nil 120 } 121 122 func FindIngressURL(client kubernetes.Interface, namespace string, name string) (string, error) { 123 log.Logger().Debugf("Finding ingress url for %s in namespace %s", name, namespace) 124 // lets try find the service via Ingress 125 ing, err := client.ExtensionsV1beta1().Ingresses(namespace).Get(name, meta_v1.GetOptions{}) 126 if err != nil { 127 log.Logger().Debugf("Error finding ingress for %s in namespace %s - err %s", name, namespace, err) 128 return "", nil 129 } 130 131 url := IngressURL(ing) 132 if url == "" { 133 log.Logger().Debugf("Unable to find url via ingress for %s in namespace %s", name, namespace) 134 } 135 return url, nil 136 } 137 138 // IngressURL returns the URL for the ingres 139 func IngressURL(ing *v1beta1.Ingress) string { 140 if ing != nil { 141 if len(ing.Spec.Rules) > 0 { 142 rule := ing.Spec.Rules[0] 143 hostname := rule.Host 144 for _, tls := range ing.Spec.TLS { 145 for _, h := range tls.Hosts { 146 if h != "" { 147 url := "https://" + h 148 log.Logger().Debugf("found service url %s", url) 149 return url 150 } 151 } 152 } 153 if hostname != "" { 154 url := "http://" + hostname 155 log.Logger().Debugf("found service url %s", url) 156 return url 157 } 158 } 159 } 160 return "" 161 } 162 163 // IngressHost returns the host for the ingres 164 func IngressHost(ing *v1beta1.Ingress) string { 165 if ing != nil { 166 if len(ing.Spec.Rules) > 0 { 167 rule := ing.Spec.Rules[0] 168 hostname := rule.Host 169 for _, tls := range ing.Spec.TLS { 170 for _, h := range tls.Hosts { 171 if h != "" { 172 return h 173 } 174 } 175 } 176 if hostname != "" { 177 return hostname 178 } 179 } 180 } 181 return "" 182 } 183 184 // IngressProtocol returns the scheme (https / http) for the Ingress 185 func IngressProtocol(ing *v1beta1.Ingress) string { 186 if ing != nil && len(ing.Spec.TLS) == 0 { 187 return "http" 188 } 189 return "https" 190 } 191 192 func FindServiceHostname(client kubernetes.Interface, namespace string, name string) (string, error) { 193 // lets try find the service via Ingress 194 ing, err := client.ExtensionsV1beta1().Ingresses(namespace).Get(name, meta_v1.GetOptions{}) 195 if ing != nil && err == nil { 196 if len(ing.Spec.Rules) > 0 { 197 rule := ing.Spec.Rules[0] 198 hostname := rule.Host 199 for _, tls := range ing.Spec.TLS { 200 for _, h := range tls.Hosts { 201 if h != "" { 202 return h, nil 203 } 204 } 205 } 206 if hostname != "" { 207 return hostname, nil 208 } 209 } 210 } 211 return "", nil 212 } 213 214 // FindService looks up a service by name across all namespaces 215 func FindService(client kubernetes.Interface, name string) (*v1.Service, error) { 216 nsl, err := client.CoreV1().Namespaces().List(meta_v1.ListOptions{}) 217 if err != nil { 218 return nil, err 219 } 220 for _, ns := range nsl.Items { 221 svc, err := client.CoreV1().Services(ns.GetName()).Get(name, meta_v1.GetOptions{}) 222 if err == nil { 223 return svc, nil 224 } 225 } 226 return nil, errors.New("Service not found!") 227 } 228 229 // GetServiceURL returns the 230 func GetServiceURL(svc *v1.Service) string { 231 url := "" 232 if svc != nil && svc.Annotations != nil { 233 url = svc.Annotations[ExposeURLAnnotation] 234 } 235 if url == "" { 236 scheme := "http" 237 for _, port := range svc.Spec.Ports { 238 if port.Port == 443 { 239 scheme = "https" 240 break 241 } 242 } 243 244 // lets check if its a LoadBalancer 245 if svc.Spec.Type == v1.ServiceTypeLoadBalancer { 246 for _, ing := range svc.Status.LoadBalancer.Ingress { 247 if ing.IP != "" { 248 return scheme + "://" + ing.IP + "/" 249 } 250 if ing.Hostname != "" { 251 return scheme + "://" + ing.Hostname + "/" 252 } 253 } 254 } 255 } 256 return url 257 } 258 259 // FindServiceSchemePort parses the service definition and interprets http scheme in the absence of an external ingress 260 func FindServiceSchemePort(client kubernetes.Interface, namespace string, name string) (string, string, error) { 261 svc, err := client.CoreV1().Services(namespace).Get(name, meta_v1.GetOptions{}) 262 if err != nil { 263 return "", "", errors.Wrapf(err, "failed to find service %s in namespace %s", name, namespace) 264 } 265 return ExtractServiceSchemePort(svc) 266 } 267 268 func GetServiceURLFromName(c kubernetes.Interface, name, ns string) (string, error) { 269 return FindServiceURL(c, ns, name) 270 } 271 272 func FindServiceURLs(client kubernetes.Interface, namespace string) ([]ServiceURL, error) { 273 options := meta_v1.ListOptions{} 274 urls := []ServiceURL{} 275 svcs, err := client.CoreV1().Services(namespace).List(options) 276 if err != nil { 277 return urls, err 278 } 279 for _, s := range svcs.Items { 280 svc := s 281 url := GetServiceURL(&svc) 282 if url == "" { 283 url, _ = FindServiceURL(client, namespace, svc.Name) 284 } 285 if len(url) > 0 { 286 urls = append(urls, ServiceURL{ 287 Name: svc.Name, 288 URL: url, 289 }) 290 } 291 } 292 return urls, nil 293 } 294 295 // WaitForExternalIP waits for the pods of a deployment to become ready 296 func WaitForExternalIP(client kubernetes.Interface, name, namespace string, timeout time.Duration) error { 297 298 options := meta_v1.ListOptions{ 299 FieldSelector: fields.OneTermEqualSelector("metadata.name", name).String(), 300 } 301 302 w, err := client.CoreV1().Services(namespace).Watch(options) 303 304 if err != nil { 305 return err 306 } 307 defer w.Stop() 308 309 condition := func(event watch.Event) (bool, error) { 310 svc := event.Object.(*v1.Service) 311 return HasExternalAddress(svc), nil 312 } 313 314 ctx, _ := context.WithTimeout(context.Background(), timeout) 315 _, err = tools_watch.UntilWithoutRetry(ctx, w, condition) 316 317 if err == wait.ErrWaitTimeout { 318 return fmt.Errorf("service %s never became ready", name) 319 } 320 return nil 321 } 322 323 // WaitForService waits for a service to become ready 324 func WaitForService(client kubernetes.Interface, name, namespace string, timeout time.Duration) error { 325 options := meta_v1.ListOptions{ 326 FieldSelector: fields.OneTermEqualSelector("metadata.name", name).String(), 327 } 328 w, err := client.CoreV1().Services(namespace).Watch(options) 329 if err != nil { 330 return err 331 } 332 defer w.Stop() 333 334 condition := func(event watch.Event) (bool, error) { 335 svc := event.Object.(*v1.Service) 336 return svc.GetName() == name, nil 337 } 338 ctx, _ := context.WithTimeout(context.Background(), timeout) 339 _, err = tools_watch.UntilWithoutRetry(ctx, w, condition) 340 341 if err == wait.ErrWaitTimeout { 342 return fmt.Errorf("service %s never became ready", name) 343 } 344 345 return nil 346 } 347 348 func HasExternalAddress(svc *v1.Service) bool { 349 for _, v := range svc.Status.LoadBalancer.Ingress { 350 if v.IP != "" || v.Hostname != "" { 351 return true 352 } 353 } 354 return false 355 } 356 357 func CreateServiceLink(client kubernetes.Interface, currentNamespace, targetNamespace, serviceName, externalURL string) error { 358 annotations := make(map[string]string) 359 annotations[ExposeURLAnnotation] = externalURL 360 361 svc := v1.Service{ 362 ObjectMeta: meta_v1.ObjectMeta{ 363 Name: serviceName, 364 Namespace: currentNamespace, 365 Annotations: annotations, 366 }, 367 Spec: v1.ServiceSpec{ 368 Type: v1.ServiceTypeExternalName, 369 ExternalName: fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, targetNamespace), 370 }, 371 } 372 373 _, err := client.CoreV1().Services(currentNamespace).Create(&svc) 374 if err != nil { 375 return err 376 } 377 378 return nil 379 } 380 381 func DeleteService(client *kubernetes.Clientset, namespace string, serviceName string) error { 382 return client.CoreV1().Services(namespace).Delete(serviceName, &meta_v1.DeleteOptions{}) 383 } 384 385 func GetService(client kubernetes.Interface, currentNamespace, targetNamespace, serviceName string) error { 386 svc := v1.Service{ 387 ObjectMeta: meta_v1.ObjectMeta{ 388 Name: serviceName, 389 Namespace: currentNamespace, 390 }, 391 Spec: v1.ServiceSpec{ 392 Type: v1.ServiceTypeExternalName, 393 ExternalName: fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, targetNamespace), 394 }, 395 } 396 _, err := client.CoreV1().Services(currentNamespace).Create(&svc) 397 if err != nil { 398 return err 399 } 400 return nil 401 } 402 403 func IsServicePresent(c kubernetes.Interface, name, ns string) (bool, error) { 404 svc, err := c.CoreV1().Services(ns).Get(name, meta_v1.GetOptions{}) 405 if err != nil || svc == nil { 406 return false, err 407 } 408 return true, nil 409 } 410 411 // GetServiceAppName retrieves the application name from the service labels 412 func GetServiceAppName(c kubernetes.Interface, name, ns string) (string, error) { 413 svc, err := c.CoreV1().Services(ns).Get(name, meta_v1.GetOptions{}) 414 if err != nil || svc == nil { 415 return "", errors.Wrapf(err, "retrieving service %q", name) 416 } 417 return ServiceAppName(svc), nil 418 } 419 420 // ServiceAppName retrives the application name from service labels. If no app lable exists, 421 // it returns the service name 422 func ServiceAppName(service *v1.Service) string { 423 if annotations := service.Annotations; annotations != nil { 424 ingName, ok := annotations[ExposeIngressName] 425 if ok { 426 return ingName 427 } 428 } 429 if labels := service.Labels; labels != nil { 430 app, ok := labels[ServiceAppLabel] 431 if ok { 432 return app 433 } 434 } 435 return service.GetName() 436 } 437 438 // AnnotateServicesWithCertManagerIssuer adds the cert-manager annotation to the services from the given namespace. If a list of 439 // services is provided, it will apply the annotation only to that specific services. 440 func AnnotateServicesWithCertManagerIssuer(c kubernetes.Interface, ns, issuer string, clusterIssuer bool, services ...string) ([]*v1.Service, error) { 441 result := make([]*v1.Service, 0) 442 svcList, err := GetServices(c, ns) 443 if err != nil { 444 return result, err 445 } 446 447 for _, s := range svcList { 448 // annotate only the services present in the list, if the list is empty annotate all services 449 if len(services) > 0 { 450 i := util.StringArrayIndex(services, s.GetName()) 451 if i < 0 { 452 continue 453 } 454 } 455 if s.Annotations[ExposeAnnotation] == "true" && s.Annotations[JenkinsXSkipTLSAnnotation] != "true" { 456 existingAnnotations, _ := s.Annotations[ExposeIngressAnnotation] 457 // if no existing `fabric8.io/ingress.annotations` initialise and add else update with ClusterIssuer 458 certManagerAnnotation := CertManagerAnnotation 459 if clusterIssuer == true { 460 certManagerAnnotation = CertManagerClusterAnnotation 461 } 462 if len(existingAnnotations) > 0 { 463 s.Annotations[ExposeIngressAnnotation] = existingAnnotations + "\n" + certManagerAnnotation + ": " + issuer 464 } else { 465 s.Annotations[ExposeIngressAnnotation] = certManagerAnnotation + ": " + issuer 466 } 467 s, err = c.CoreV1().Services(ns).Update(s) 468 if err != nil { 469 return result, fmt.Errorf("failed to annotate and update service %s in namespace %s: %v", s.Name, ns, err) 470 } 471 result = append(result, s) 472 } 473 } 474 return result, nil 475 } 476 477 // AnnotateServicesWithBasicAuth annotates the services with nginx baisc auth annotations 478 func AnnotateServicesWithBasicAuth(client kubernetes.Interface, ns string, services ...string) error { 479 if len(services) == 0 { 480 return nil 481 } 482 svcList, err := GetServices(client, ns) 483 if err != nil { 484 return errors.Wrapf(err, "retrieving the services from namespace %q", ns) 485 } 486 for _, service := range svcList { 487 // Check if the service is in the white-list 488 idx := util.StringArrayIndex(services, service.GetName()) 489 if idx < 0 { 490 continue 491 } 492 if service.Annotations == nil { 493 service.Annotations = map[string]string{} 494 } 495 // Add the required basic authentication annotation for nginx-ingress controller 496 ingressAnnotations := service.Annotations[ExposeIngressAnnotation] 497 basicAuthAnnotations := fmt.Sprintf( 498 "nginx.ingress.kubernetes.io/auth-type: basic\nnginx.ingress.kubernetes.io/auth-secret: %s\nnginx.ingress.kubernetes.io/auth-realm: Authentication is required to access this service", 499 kube.SecretBasicAuth) 500 if ingressAnnotations != "" { 501 ingressAnnotations = ingressAnnotations + "\n" + basicAuthAnnotations 502 } else { 503 ingressAnnotations = basicAuthAnnotations 504 } 505 service.Annotations[ExposeIngressAnnotation] = ingressAnnotations 506 _, err = client.CoreV1().Services(ns).Update(service) 507 if err != nil { 508 return errors.Wrapf(err, "updating the service %q in namesapce %q", service.GetName(), ns) 509 } 510 } 511 return nil 512 } 513 514 func CleanServiceAnnotations(c kubernetes.Interface, ns string, services ...string) error { 515 svcList, err := GetServices(c, ns) 516 if err != nil { 517 return err 518 } 519 for _, s := range svcList { 520 // clear the annotations only for the services provided in the list if the list 521 // is not empty, otherwise clear the annotations of all services 522 if len(services) > 0 { 523 i := util.StringArrayIndex(services, s.GetName()) 524 if i < 0 { 525 continue 526 } 527 } 528 if s.Annotations[ExposeAnnotation] == "true" && s.Annotations[JenkinsXSkipTLSAnnotation] != "true" { 529 // if no existing `fabric8.io/ingress.annotations` initialise and add else update with ClusterIssuer 530 annotationsForIngress := s.Annotations[ExposeIngressAnnotation] 531 if len(annotationsForIngress) > 0 { 532 533 var newAnnotations []string 534 annotations := strings.Split(annotationsForIngress, "\n") 535 for _, element := range annotations { 536 annotation := strings.SplitN(element, ":", 2) 537 key, _ := annotation[0], strings.TrimSpace(annotation[1]) 538 if key != CertManagerAnnotation && key != CertManagerClusterAnnotation { 539 newAnnotations = append(newAnnotations, element) 540 } 541 } 542 annotationsForIngress = "" 543 for _, v := range newAnnotations { 544 if len(annotationsForIngress) > 0 { 545 annotationsForIngress = annotationsForIngress + "\n" + v 546 } else { 547 annotationsForIngress = v 548 } 549 } 550 s.Annotations[ExposeIngressAnnotation] = annotationsForIngress 551 552 } 553 delete(s.Annotations, ExposeURLAnnotation) 554 555 _, err = c.CoreV1().Services(ns).Update(s) 556 if err != nil { 557 return fmt.Errorf("failed to clean service %s annotations in namespace %s: %v", s.Name, ns, err) 558 } 559 } 560 } 561 return nil 562 } 563 564 // ExtractServiceSchemePort is a utility function to interpret http scheme and port information from k8s service definitions 565 func ExtractServiceSchemePort(svc *v1.Service) (string, string, error) { 566 scheme := "" 567 port := "" 568 569 found := false 570 571 // Search in order of degrading priority 572 for _, p := range svc.Spec.Ports { 573 if p.Port == 443 { // Prefer 443/https if found 574 scheme = "https" 575 port = "443" 576 found = true 577 break 578 } 579 } 580 581 if !found { 582 for _, p := range svc.Spec.Ports { 583 if p.Port == 80 { // Use 80/http if found 584 scheme = "http" 585 port = "80" 586 found = true 587 } 588 } 589 } 590 591 if !found { // No conventional ports, so search for named https ports 592 for _, p := range svc.Spec.Ports { 593 if p.Protocol == "TCP" { 594 if p.Name == "https" { 595 scheme = "https" 596 port = strconv.FormatInt(int64(p.Port), 10) 597 found = true 598 break 599 } 600 } 601 } 602 } 603 604 if !found { // No conventional ports, so search for named http ports 605 for _, p := range svc.Spec.Ports { 606 if p.Name == "http" { 607 scheme = "http" 608 port = strconv.FormatInt(int64(p.Port), 10) 609 found = true 610 break 611 } 612 } 613 } 614 615 return scheme, port, nil 616 }