github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/opts/upgrade/upgrade_ingress.go (about) 1 package upgrade 2 3 import ( 4 "context" 5 "fmt" 6 7 "github.com/olli-ai/jx/v2/pkg/cmd/opts" 8 "github.com/olli-ai/jx/v2/pkg/cmd/update" 9 10 "strings" 11 "time" 12 13 "github.com/olli-ai/jx/v2/pkg/gits" 14 "github.com/olli-ai/jx/v2/pkg/kube/pki" 15 "github.com/olli-ai/jx/v2/pkg/kube/services" 16 "github.com/pkg/errors" 17 18 "github.com/jenkins-x/jx-logging/pkg/log" 19 "github.com/olli-ai/jx/v2/pkg/kube" 20 "github.com/olli-ai/jx/v2/pkg/util" 21 survey "gopkg.in/AlecAivazis/survey.v1" 22 v1 "k8s.io/api/core/v1" 23 "k8s.io/api/extensions/v1beta1" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 ) 26 27 const ( 28 exposecontroller = "exposecontroller" 29 30 certsIssuedReadyTimeout = 5 * time.Minute 31 ) 32 33 // UpgradeIngressOptions the options for the create spring command 34 type UpgradeIngressOptions struct { 35 *opts.CommonOptions 36 37 SkipCertManager bool 38 Cluster bool 39 Force bool 40 Namespaces []string 41 Version string 42 TargetNamespaces []string 43 Services []string 44 SkipResourcesUpdate bool 45 WaitForCerts bool 46 ConfigNamespace string 47 48 IngressConfig kube.IngressConfig 49 } 50 51 // Run implements the command 52 func (o *UpgradeIngressOptions) Run() error { 53 client, devNamespace, err := o.KubeClientAndDevNamespace() 54 if err != nil { 55 return fmt.Errorf("cannot connect to Kubernetes cluster: %v", err) 56 } 57 58 jxClient, ns, err := o.JXClient() 59 if err != nil { 60 return errors.Wrap(err, "error obtaining the JX Client") 61 } 62 63 devEnv, err := jxClient.JenkinsV1().Environments(ns).Get("dev", metav1.GetOptions{}) 64 if err != nil { 65 return errors.Wrap(err, "error obtaining the ") 66 } 67 68 //Todo: Possibly used with jx install, if so, it should not be removed now 69 if devEnv.Spec.TeamSettings.BootRequirements != "" { 70 return errors.New(`jx upgrade ingress shouldn't be used in a Jenkins X Boot cluster. 71 For more documentation on Ingress configuration see: [https://jenkins-x.io/docs/getting-started/setup/boot/#ingress](https://jenkins-x.io/docs/getting-started/setup/boot/#ingress)`) 72 } 73 74 previousWebHookEndpoint := "" 75 if !o.SkipResourcesUpdate { 76 previousWebHookEndpoint, err = o.GetWebHookEndpoint() 77 if err != nil { 78 return errors.Wrap(err, "getting the webhook endpoint") 79 } 80 } 81 82 // if existing ingress exist in the namespaces ask do you want to delete them? 83 ingressToDelete, err := o.getExistingIngressRules() 84 if err != nil { 85 return errors.Wrap(err, "getting the existing ingress rules") 86 } 87 88 // wizard to ask for config values 89 err = o.confirmExposecontrollerConfig() 90 if err != nil { 91 return errors.Wrap(err, ""+ 92 "configure exposecontroller") 93 } 94 95 // confirm values 96 if !o.BatchMode { 97 if answer, err := util.Confirm(fmt.Sprintf("Using config values %v, ok?", o.IngressConfig), true, "", o.GetIOFileHandles()); err != nil { 98 return err 99 } else if !answer { 100 log.Logger().Infof("Terminating") 101 return nil 102 } 103 } 104 105 // save details to a configmap 106 _, err = kube.SaveAsConfigMap(client, kube.ConfigMapIngressConfig, devNamespace, o.IngressConfig) 107 if err != nil { 108 return errors.Wrap(err, "saving ingress config into a configmap") 109 } 110 111 // ensure cert-manager is installed 112 if o.IngressConfig.TLS { 113 err = o.ensureCertmanagerSetup() 114 if err != nil { 115 return errors.Wrap(err, "ensure cert-manager setup") 116 } 117 } 118 119 // clear the service annotations 120 err = o.CleanServiceAnnotations(o.Services...) 121 if err != nil { 122 return errors.Wrap(err, "cleaning service annotations") 123 } 124 125 // annotate any service that has expose=true with correct cert-manager staging / prod annotation 126 var services []*v1.Service 127 if o.IngressConfig.TLS { 128 services, err = o.AnnotateExposedServicesWithCertManager(o.Services...) 129 if err != nil { 130 return errors.Wrap(err, "annotating the exposed service with cert-manager") 131 } 132 } 133 134 // remove the ingress resource in order to allow the ingress-controller to recreate them 135 for name, namespace := range ingressToDelete { 136 log.Logger().Infof("Deleting ingress %s/%s", namespace, name) 137 err := client.ExtensionsV1beta1().Ingresses(namespace).Delete(name, &metav1.DeleteOptions{}) 138 if err != nil { 139 return fmt.Errorf("cannot delete ingress rule %s in namespace %s: %v", name, namespace, err) 140 } 141 } 142 143 // start watching and collecting ready certificates 144 var notReadyCertsCh <-chan map[pki.Certificate]bool 145 ctx, cancel := context.WithTimeout(context.Background(), certsIssuedReadyTimeout) 146 defer cancel() 147 if o.IngressConfig.TLS && o.WaitForCerts { 148 certsCh, err := o.watchReadyCertificates(ctx) 149 if err != nil { 150 return errors.Wrap(err, "start watching ready certificates") 151 } 152 notReadyCertsCh = o.startCollectingReadyCertificates(ctx, services, certsCh) 153 } 154 155 // run the expose-controller to create the ingress rules 156 err = o.createIngressRules() 157 if err != nil { 158 return errors.Wrap(err, "creating the ingress rules") 159 } 160 161 log.Logger().Info("Ingress rules recreated") 162 163 if o.IngressConfig.TLS { 164 if o.WaitForCerts { 165 log.Logger().Info("Waiting for TLS certificates to be issued...") 166 select { 167 case certs := <-notReadyCertsCh: 168 cancel() 169 if len(certs) == 0 { 170 log.Logger().Info("All TLS certificates are ready") 171 } else { 172 log.Logger().Warn("Following TLS certificates are not ready:") 173 for cert := range certs { 174 log.Logger().Warnf("%s", cert) 175 } 176 return errors.New("not all TLS certificates are ready") 177 } 178 case <-ctx.Done(): 179 log.Logger().Warn("Timeout reached while waiting for TLS certificates to be ready") 180 } 181 } else { 182 log.Logger().Warn("It can take around 5 minutes for Cert Manager to get certificates from Lets Encrypt and update Ingress rules") 183 log.Logger().Info("Use the following commands to diagnose any issues:") 184 log.Logger().Infof("jx logs %s -n %s", pki.CertManagerDeployment, pki.CertManagerNamespace) 185 log.Logger().Info("kubectl describe certificates") 186 log.Logger().Info("kubectl describe issuers\n") 187 } 188 } 189 190 // update all resource dependent to the ingress endpoints 191 if !o.SkipResourcesUpdate { 192 err = o.updateResources(previousWebHookEndpoint) 193 if err != nil { 194 return errors.Wrap(err, "unable to update resources for webhook change") 195 } 196 } 197 198 return nil 199 } 200 201 func (o *UpgradeIngressOptions) watchReadyCertificates(ctx context.Context) (<-chan pki.Certificate, error) { 202 client, err := o.CertManagerClient() 203 if err != nil { 204 return nil, errors.Wrap(err, "creating the cert-manager client") 205 } 206 207 // watch certificates across all namesapces 208 namespace := "" 209 certsCh, err := pki.WatchCertificatesIssuedReady(ctx, client, namespace) 210 if err != nil { 211 return nil, errors.Wrap(err, "start watching certificates") 212 } 213 return certsCh, nil 214 } 215 216 func (o *UpgradeIngressOptions) startCollectingReadyCertificates(ctx context.Context, services []*v1.Service, 217 certsCh <-chan pki.Certificate) <-chan map[pki.Certificate]bool { 218 resultCh := make(chan map[pki.Certificate]bool) 219 go func() { 220 certs := pki.ToCertificates(services) 221 certsMap := make(map[pki.Certificate]bool) 222 for _, cert := range certs { 223 certsMap[cert] = true 224 } 225 226 log.Logger().Infof("Expecting certificates: %v", certs) 227 228 for { 229 select { 230 case cert := <-certsCh: 231 log.Logger().Infof("Ready Cert: %s", util.ColorInfo(cert)) 232 delete(certsMap, cert) 233 // check if all expected certificates are received 234 if len(certsMap) == 0 { 235 // send a map with no certificates to indicate success 236 resultCh <- certsMap 237 return 238 } 239 case <-ctx.Done(): 240 // send the current state of the certificates map 241 resultCh <- certsMap 242 return 243 } 244 } 245 }() 246 return resultCh 247 } 248 249 func (o *UpgradeIngressOptions) updateResources(previousWebHookEndpoint string) error { 250 _, _, err := o.JXClient() 251 if err != nil { 252 return errors.Wrap(err, "failed to get jxclient") 253 } 254 255 isProwEnabled, err := o.IsProw() 256 if err != nil { 257 return errors.Wrap(err, "checking if is prow") 258 } 259 260 if !isProwEnabled { 261 err = o.UpdateJenkinsURL(o.TargetNamespaces) 262 if err != nil { 263 return errors.Wrap(err, "upgrade jenkins URL") 264 } 265 } 266 267 updatedWebHookEndpoint, err := o.GetWebHookEndpoint() 268 if err != nil { 269 return errors.Wrap(err, "retrieving the webhook endpoint") 270 } 271 272 log.Logger().Infof("Previous webhook endpoint %s", previousWebHookEndpoint) 273 log.Logger().Infof("Updated webhook endpoint %s", updatedWebHookEndpoint) 274 updateWebHooks := true 275 if !o.BatchMode { 276 if answer, err := util.Confirm("Do you want to update all existing webhooks?", true, "", o.GetIOFileHandles()); err != nil { 277 return err 278 } else if !answer { 279 updateWebHooks = false 280 } 281 } 282 283 if updateWebHooks { 284 err := o.updateWebHooks(previousWebHookEndpoint, updatedWebHookEndpoint) 285 if err != nil { 286 return errors.Wrap(err, "unable to update webhooks") 287 } 288 } 289 return nil 290 } 291 292 func (o *UpgradeIngressOptions) isIngressForServices(ingress *v1beta1.Ingress) bool { 293 services := o.Services 294 if len(services) == 0 { 295 // allow all ingresses if no services filter is defined 296 return true 297 } 298 rules := ingress.Spec.Rules 299 for _, rule := range rules { 300 http := rule.IngressRuleValue.HTTP 301 if http == nil { 302 continue 303 } 304 for _, path := range http.Paths { 305 service := path.Backend.ServiceName 306 i := util.StringArrayIndex(services, service) 307 if i >= 0 { 308 return true 309 } 310 } 311 } 312 return false 313 } 314 315 func (o *UpgradeIngressOptions) getExistingIngressRules() (map[string]string, error) { 316 surveyOpts := survey.WithStdio(o.In, o.Out, o.Err) 317 existingIngressNames := map[string]string{} 318 client, currentNamespace, err := o.KubeClientAndNamespace() 319 if err != nil { 320 return existingIngressNames, err 321 } 322 var confirmMessage string 323 if o.Cluster { 324 confirmMessage = "Existing ingress rules found in the cluster. Confirm to delete all and recreate them" 325 326 ings, err := client.ExtensionsV1beta1().Ingresses("").List(metav1.ListOptions{}) 327 if err != nil { 328 return existingIngressNames, fmt.Errorf("cannot list all ingresses in cluster: %v", err) 329 } 330 for _, i := range ings.Items { 331 item := i 332 if item.Annotations[services.ExposeGeneratedByAnnotation] == exposecontroller { 333 if o.isIngressForServices(&item) { 334 existingIngressNames[item.Name] = item.Namespace 335 } 336 } 337 } 338 339 nsList, err := client.CoreV1().Namespaces().List(metav1.ListOptions{}) 340 for _, n := range nsList.Items { 341 o.TargetNamespaces = append(o.TargetNamespaces, n.Name) 342 } 343 344 } else if len(o.Namespaces) > 0 { 345 confirmMessage = fmt.Sprintf("Existing ingress rules found in namespaces %v namespace. Confirm to delete and recreate them", o.Namespaces) 346 // loop round each 347 for _, n := range o.Namespaces { 348 ings, err := client.ExtensionsV1beta1().Ingresses(n).List(metav1.ListOptions{}) 349 if err != nil { 350 return existingIngressNames, fmt.Errorf("cannot list all ingresses in cluster: %v", err) 351 } 352 for _, i := range ings.Items { 353 item := i 354 if i.Annotations[services.ExposeGeneratedByAnnotation] == exposecontroller { 355 if o.isIngressForServices(&item) { 356 existingIngressNames[item.Name] = item.Namespace 357 } 358 } 359 } 360 o.TargetNamespaces = append(o.TargetNamespaces, n) 361 } 362 } else { 363 confirmMessage = "Existing ingress rules found in current namespace. Confirm to delete and recreate them" 364 // fall back to current ns only 365 log.Logger().Infof("Looking for existing ingress rules in current namespace %s", currentNamespace) 366 367 ings, err := client.ExtensionsV1beta1().Ingresses(currentNamespace).List(metav1.ListOptions{}) 368 if err != nil { 369 return existingIngressNames, fmt.Errorf("cannot list all ingresses in cluster: %v", err) 370 } 371 for _, i := range ings.Items { 372 item := i 373 if i.Annotations[services.ExposeGeneratedByAnnotation] == exposecontroller { 374 if o.isIngressForServices(&item) { 375 existingIngressNames[item.Name] = item.Namespace 376 } 377 } 378 } 379 o.TargetNamespaces = append(o.TargetNamespaces, currentNamespace) 380 } 381 382 if len(existingIngressNames) == 0 { 383 return existingIngressNames, nil 384 } 385 386 if !o.BatchMode { 387 confirm := &survey.Confirm{ 388 Message: confirmMessage, 389 Default: true, 390 } 391 flag := true 392 err = survey.AskOne(confirm, &flag, nil, surveyOpts) 393 if err != nil { 394 return existingIngressNames, err 395 } 396 if !flag { 397 return existingIngressNames, errors.New("Not able to automatically delete existing ingress rules. Either delete manually or change the scope the command should run in") 398 } 399 } 400 401 return existingIngressNames, nil 402 } 403 404 func (o *UpgradeIngressOptions) confirmExposecontrollerConfig() error { 405 // get current ingress config to use as existing defaults 406 client, currentNamespace, err := o.KubeClientAndNamespace() 407 if err != nil { 408 return err 409 } 410 411 // select the namespace from where to read the ingress-config config map 412 devNamespace, _, err := kube.GetDevNamespace(client, currentNamespace) 413 if err != nil { 414 return fmt.Errorf("cannot find a dev team namespace to get existing exposecontroller config from. %v", err) 415 } 416 configNamespace := devNamespace 417 if o.ConfigNamespace != "" { 418 configNamespace = o.ConfigNamespace 419 } 420 421 // Overwrites the ingress config with the values from config map only if this config map exists 422 urlTemplate := o.IngressConfig.UrlTemplate 423 domain := o.IngressConfig.Domain 424 ic, err := kube.GetIngressConfig(client, configNamespace) 425 if err == nil { 426 // TODO: Add the rest of the Ingress-related info as arguments and assign to `o.IngressConfig` only those that were not specified, instead of the whole `ic`.` 427 o.IngressConfig = ic 428 if urlTemplate != "" { 429 // Template must be surrounded by quotes 430 if !strings.HasPrefix(urlTemplate, "\"") && !strings.HasPrefix(urlTemplate, "'") { 431 urlTemplate = "\"" + urlTemplate + "\"" 432 } 433 o.IngressConfig.UrlTemplate = urlTemplate 434 } 435 if domain != "" { 436 o.IngressConfig.Domain = domain 437 } 438 } 439 440 if o.BatchMode { 441 if err := checkEmtptyIngressConfig(o.IngressConfig.Exposer, "exposer"); err != nil { 442 return err 443 } 444 if err := checkEmtptyIngressConfig(o.IngressConfig.Domain, "domain"); err != nil { 445 return err 446 } 447 if o.IngressConfig.TLS { 448 if err := checkEmtptyIngressConfig(o.IngressConfig.Issuer, "issuer"); err != nil { 449 return err 450 } 451 if err := checkEmtptyIngressConfig(o.IngressConfig.Email, "email"); err != nil { 452 return err 453 } 454 } 455 } else { 456 o.IngressConfig.Exposer, err = util.PickNameWithDefault([]string{"Ingress", "Route"}, "Expose type", o.IngressConfig.Exposer, "", o.GetIOFileHandles()) 457 if err != nil { 458 return err 459 } 460 461 o.IngressConfig.Domain, err = util.PickValue("Domain:", o.IngressConfig.Domain, true, "", o.GetIOFileHandles()) 462 if err != nil { 463 return err 464 } 465 466 if !strings.HasSuffix(o.IngressConfig.Domain, "nip.io") { 467 if !o.BatchMode { 468 o.IngressConfig.TLS, err = util.Confirm("If your network is publicly available would you like to enable cluster wide TLS?", true, "Enables cert-manager and configures TLS with signed certificates from LetsEncrypt", o.GetIOFileHandles()) 469 if err != nil { 470 return err 471 } 472 } 473 474 if o.IngressConfig.TLS { 475 log.Logger().Infof("If testing LetsEncrypt you should use staging as you may be rate limited using production.") 476 clusterIssuer, err := util.PickNameWithDefault([]string{"staging", "production"}, "Use LetsEncrypt staging or production?", "production", "", o.GetIOFileHandles()) 477 // if the cluster issuer is production the string needed by letsencrypt is prod 478 if clusterIssuer == "production" { 479 clusterIssuer = "prod" 480 } 481 if err != nil { 482 return err 483 } 484 o.IngressConfig.Issuer = "letsencrypt-" + clusterIssuer 485 486 if o.IngressConfig.Email == "" { 487 email1, err := o.GetCommandOutput("", "git", "config", "user.email") 488 if err != nil { 489 return err 490 } 491 492 o.IngressConfig.Email = strings.TrimSpace(email1) 493 } 494 495 o.IngressConfig.Email, err = util.PickValue("Email address to register with LetsEncrypt:", o.IngressConfig.Email, true, "", o.GetIOFileHandles()) 496 if err != nil { 497 return err 498 } 499 } 500 } 501 o.IngressConfig.UrlTemplate, err = util.PickValue("URLTemplate (press <Enter> to keep the current value):", o.IngressConfig.UrlTemplate, false, "", o.GetIOFileHandles()) 502 if err != nil { 503 return err 504 } 505 } 506 507 return nil 508 } 509 510 func checkEmtptyIngressConfig(value string, name string) error { 511 if value == "" { 512 return fmt.Errorf("%v config value must not be empty", name) 513 } 514 return nil 515 } 516 517 func (o *UpgradeIngressOptions) createIngressRules() error { 518 client, currentNamespace, err := o.KubeClientAndNamespace() 519 if err != nil { 520 return err 521 } 522 certmngClient, err := o.CertManagerClient() 523 if err != nil { 524 return errors.Wrap(err, "creating the cert-manager client") 525 } 526 devNamespace, _, err := kube.GetDevNamespace(client, currentNamespace) 527 if err != nil { 528 return fmt.Errorf("cannot find a dev team namespace to get existing exposecontroller config from. %v", err) 529 } 530 for _, n := range o.TargetNamespaces { 531 o.CleanExposecontrollerReources(n) 532 533 if len(o.Services) > 0 { 534 services, err := services.GetServicesByName(client, n, o.Services) 535 if err != nil { 536 return err 537 } 538 certs := pki.ToCertificates(services) 539 err = pki.CleanCerts(client, certmngClient, n, certs) 540 if err != nil { 541 return err 542 } 543 } else { 544 err := pki.CleanAllCerts(client, certmngClient, n) 545 if err != nil { 546 return err 547 } 548 } 549 550 err := pki.CreateCertManagerResources(certmngClient, n, o.IngressConfig) 551 if err != nil { 552 return err 553 } 554 555 err = o.RunExposecontroller(devNamespace, n, o.IngressConfig, o.Services...) 556 if err != nil { 557 return err 558 } 559 } 560 return nil 561 } 562 563 func (o *UpgradeIngressOptions) ensureCertmanagerSetup() error { 564 if !o.SkipCertManager { 565 return o.EnsureCertManager() 566 } 567 return nil 568 } 569 570 // AnnotateExposedServicesWithCertManager annotates exposed services with cert manager 571 func (o *UpgradeIngressOptions) AnnotateExposedServicesWithCertManager(svcs ...string) ([]*v1.Service, error) { 572 result := make([]*v1.Service, 0) 573 client, err := o.KubeClient() 574 if err != nil { 575 return result, err 576 } 577 for _, n := range o.TargetNamespaces { 578 issuer := o.IngressConfig.Issuer 579 if issuer == "" { 580 return result, fmt.Errorf("no issuer was configured for cert manager") 581 } 582 clusterIssuer := o.IngressConfig.ClusterIssuer 583 services, err := services.AnnotateServicesWithCertManagerIssuer(client, n, issuer, clusterIssuer, svcs...) 584 if err != nil { 585 return result, err 586 } 587 result = append(result, services...) 588 } 589 return result, nil 590 } 591 592 // CleanServiceAnnotations cleans service annotations 593 func (o *UpgradeIngressOptions) CleanServiceAnnotations(svcs ...string) error { 594 client, err := o.KubeClient() 595 if err != nil { 596 return err 597 } 598 for _, n := range o.TargetNamespaces { 599 err := services.CleanServiceAnnotations(client, n, svcs...) 600 if err != nil { 601 return err 602 } 603 } 604 605 return nil 606 } 607 608 func (o *UpgradeIngressOptions) updateWebHooks(oldHookEndpoint string, newHookEndpoint string) error { 609 if oldHookEndpoint == newHookEndpoint && !o.Force { 610 log.Logger().Infof("Webhook URL unchanged. Use %s to force updating", util.ColorInfo("--force")) 611 return nil 612 } 613 614 log.Logger().Infof("Updating all webHooks from %s to %s", util.ColorInfo(oldHookEndpoint), util.ColorInfo(newHookEndpoint)) 615 616 updateWebHook := update.UpdateWebhooksOptions{ 617 CommonOptions: o.CommonOptions, 618 } 619 620 authConfigService, err := o.GitAuthConfigService() 621 if err != nil { 622 return errors.Wrap(err, "failed to create git auth service") 623 } 624 625 gitServer := authConfigService.Config().CurrentServer 626 git, err := o.GitProviderForGitServerURL(gitServer, "github", "") 627 if err != nil { 628 return errors.Wrap(err, "unable to determine git provider") 629 } 630 631 // user 632 userAuth := git.UserAuth() 633 username := userAuth.Username 634 635 // organisation 636 organisation, err := gits.PickOrganisation(git, username, o.GetIOFileHandles()) 637 updateWebHook.Username = ReturnUserNameIfPicked(organisation, username) 638 if err != nil { 639 return errors.Wrap(err, "unable to determine git provider") 640 } 641 642 if o.CommonOptions.Verbose { 643 log.Logger().Infof("Updating all webHooks for org %s and/or username %s", organisation, updateWebHook.Username) 644 } 645 646 updateWebHook.PreviousHookUrl = oldHookEndpoint 647 updateWebHook.Org = organisation 648 updateWebHook.DryRun = false 649 650 return updateWebHook.Run() 651 } 652 653 // ReturnUserNameIfPicked checks to see if PickOrganisation returned "" 654 // this will happen if you picked the username as organization 655 // which is valid in this scenario and allows code further down 656 // to select the appropriate API to call (user or org based) 657 func ReturnUserNameIfPicked(organisation string, username string) string { 658 if organisation == "" && username != "" { 659 return username 660 } 661 return "" 662 }