github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/preview/preview.go (about) 1 package preview 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "net/http" 7 "os" 8 "path/filepath" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/olli-ai/jx/v2/pkg/builds" 14 15 "github.com/olli-ai/jx/v2/pkg/cmd/opts/step" 16 17 "github.com/olli-ai/jx/v2/pkg/cmd/helper" 18 "github.com/olli-ai/jx/v2/pkg/cmd/promote" 19 "github.com/olli-ai/jx/v2/pkg/cmd/step/pr" 20 "github.com/olli-ai/jx/v2/pkg/kube/naming" 21 22 "github.com/pkg/errors" 23 24 "github.com/cenkalti/backoff" 25 "github.com/olli-ai/jx/v2/pkg/helm" 26 27 "github.com/olli-ai/jx/v2/pkg/kserving" 28 "github.com/olli-ai/jx/v2/pkg/users" 29 30 "github.com/olli-ai/jx/v2/pkg/kube/services" 31 32 v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1" 33 "github.com/jenkins-x/jx-logging/pkg/log" 34 "github.com/olli-ai/jx/v2/pkg/cmd/opts" 35 "github.com/olli-ai/jx/v2/pkg/cmd/templates" 36 "github.com/olli-ai/jx/v2/pkg/config" 37 "github.com/olli-ai/jx/v2/pkg/gits" 38 "github.com/olli-ai/jx/v2/pkg/kube" 39 "github.com/olli-ai/jx/v2/pkg/util" 40 "github.com/spf13/cobra" 41 batchv1 "k8s.io/api/batch/v1" 42 corev1 "k8s.io/api/core/v1" 43 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 44 "k8s.io/client-go/kubernetes" 45 kserve "knative.dev/serving/pkg/client/clientset/versioned" 46 ) 47 48 var ( 49 previewLong = templates.LongDesc(` 50 Creates or updates a Preview Environment for the given Pull Request or Branch. 51 52 For more documentation on Preview Environments see: [https://jenkins-x.io/about/features/#preview-environments](https://jenkins-x.io/about/features/#preview-environments) 53 54 `) 55 56 previewExample = templates.Examples(` 57 # Create or updates the Preview Environment for the Pull Request 58 jx preview 59 `) 60 ) 61 62 const ( 63 DOCKER_REGISTRY = "DOCKER_REGISTRY" 64 JENKINS_X_DOCKER_REGISTRY_SERVICE_HOST = "JENKINS_X_DOCKER_REGISTRY_SERVICE_HOST" 65 JENKINS_X_DOCKER_REGISTRY_SERVICE_PORT = "JENKINS_X_DOCKER_REGISTRY_SERVICE_PORT" 66 ORG = "ORG" 67 REPO_OWNER = "REPO_OWNER" 68 REPO_NAME = "REPO_NAME" 69 APP_NAME = "APP_NAME" 70 DOCKER_REGISTRY_ORG = "DOCKER_REGISTRY_ORG" 71 PREVIEW_VERSION = "PREVIEW_VERSION" 72 73 optionPostPreviewJobTimeout = "post-preview-job-timeout" 74 optionPostPreviewJobPollTime = "post-preview-poll-time" 75 optionPreviewHealthTimeout = "preview-health-timeout" 76 77 // annotationPullRequestCommentSent is the name of the annotation written on the Environment 78 // when a comment has been sent to the Pull Request - at the end of the preview deployment. 79 annotationPullRequestCommentSent = "jenkins.io/pull-request-comment-sent" 80 ) 81 82 // PreviewOptions the options for viewing running PRs 83 type PreviewOptions struct { 84 promote.PromoteOptions 85 86 Name string 87 Label string 88 Namespace string 89 DevNamespace string 90 Cluster string 91 PullRequestURL string 92 PullRequest string 93 SourceURL string 94 SourceRef string 95 Dir string 96 PostPreviewJobTimeout string 97 PostPreviewJobPollTime string 98 PreviewHealthTimeout string 99 100 PullRequestName string 101 GitConfDir string 102 GitProvider gits.GitProvider 103 GitInfo *gits.GitRepository 104 NoComment bool 105 SingleComment bool 106 107 // calculated fields 108 PostPreviewJobTimeoutDuration time.Duration 109 PostPreviewJobPollDuration time.Duration 110 PreviewHealthTimeoutDuration time.Duration 111 112 HelmValuesConfig config.HelmValuesConfig 113 114 SkipAvailabilityCheck bool 115 } 116 117 // NewCmdPreview creates a command object for the "create" command 118 func NewCmdPreview(commonOpts *opts.CommonOptions) *cobra.Command { 119 options := &PreviewOptions{ 120 HelmValuesConfig: config.HelmValuesConfig{ 121 ExposeController: &config.ExposeController{}, 122 }, 123 PromoteOptions: promote.PromoteOptions{ 124 CommonOptions: commonOpts, 125 }, 126 } 127 128 cmd := &cobra.Command{ 129 Use: "preview", 130 Short: "Creates or updates a Preview Environment for the current version of an application", 131 Long: previewLong, 132 Example: previewExample, 133 Run: func(cmd *cobra.Command, args []string) { 134 options.Cmd = cmd 135 options.Args = args 136 //Default to batch-mode when running inside the pipeline (but user override wins). 137 if !cmd.Flag(opts.OptionBatchMode).Changed { 138 commonOpts := options.PromoteOptions.CommonOptions 139 options.BatchMode = commonOpts.InCDPipeline() 140 } 141 err := options.Run() 142 helper.CheckErr(err) 143 }, 144 } 145 //addCreateAppFlags(cmd, &options.CreateOptions) 146 147 options.AddPreviewOptions(cmd) 148 options.HelmValuesConfig.AddExposeControllerValues(cmd, false) 149 options.PromoteOptions.AddPromoteOptions(cmd) 150 151 return cmd 152 } 153 154 func (o *PreviewOptions) AddPreviewOptions(cmd *cobra.Command) { 155 cmd.Flags().StringVarP(&o.Name, kube.OptionName, "n", "", "The Environment resource name. Must follow the Kubernetes name conventions like Services, Namespaces") 156 cmd.Flags().StringVarP(&o.Label, "label", "l", "", "The Environment label which is a descriptive string like 'Production' or 'Staging'") 157 cmd.Flags().StringVarP(&o.Namespace, kube.OptionNamespace, "", "", "The Kubernetes namespace for the Environment") 158 cmd.Flags().StringVarP(&o.DevNamespace, "dev-namespace", "", "", "The Developer namespace where the preview command should run") 159 cmd.Flags().StringVarP(&o.Cluster, "cluster", "c", "", "The Kubernetes cluster for the Environment. If blank and a namespace is specified assumes the current cluster") 160 cmd.Flags().StringVarP(&o.Dir, "dir", "", "", "The source directory used to detect the git source URL and reference") 161 cmd.Flags().StringVarP(&o.PullRequest, "pr", "", "", "The Pull Request Name (e.g. 'PR-23' or just '23'") 162 cmd.Flags().StringVarP(&o.PullRequestURL, "pr-url", "", "", "The Pull Request URL") 163 cmd.Flags().StringVarP(&o.SourceURL, "source-url", "s", "", "The source code git URL") 164 cmd.Flags().StringVarP(&o.SourceRef, "source-ref", "", "", "The source code git ref (branch/sha)") 165 cmd.Flags().StringVarP(&o.PostPreviewJobTimeout, optionPostPreviewJobTimeout, "", "2h", "The duration before we consider the post preview Jobs failed") 166 cmd.Flags().StringVarP(&o.PostPreviewJobPollTime, optionPostPreviewJobPollTime, "", "10s", "The amount of time between polls for the post preview Job status") 167 cmd.Flags().StringVarP(&o.PreviewHealthTimeout, optionPreviewHealthTimeout, "", "5m", "The amount of time to wait for the preview application to become healthy") 168 cmd.Flags().BoolVarP(&o.NoComment, "no-comment", "", false, "Disables commenting on the Pull Request after preview is created.") 169 cmd.Flags().BoolVarP(&o.SingleComment, "single-comment", "", false, "Comment only once on the Pull Request after preview is created - instead of one comment after each update of the preview.") 170 cmd.Flags().BoolVarP(&o.SkipAvailabilityCheck, "skip-availability-check", "", false, "Disables the mandatory availability check.") 171 } 172 173 // Run implements the command 174 func (o *PreviewOptions) Run() error { 175 var err error 176 if o.PostPreviewJobPollTime != "" { 177 o.PostPreviewJobPollDuration, err = time.ParseDuration(o.PostPreviewJobPollTime) 178 if err != nil { 179 return fmt.Errorf("Invalid duration format %s for option --%s: %s", o.PostPreviewJobPollTime, optionPostPreviewJobPollTime, err) 180 } 181 } 182 if o.PostPreviewJobTimeout != "" { 183 o.PostPreviewJobTimeoutDuration, err = time.ParseDuration(o.Timeout) 184 if err != nil { 185 return fmt.Errorf("Invalid duration format %s for option --%s: %s", o.Timeout, optionPostPreviewJobTimeout, err) 186 } 187 } 188 if o.PreviewHealthTimeout != "" { 189 o.PreviewHealthTimeoutDuration, err = time.ParseDuration(o.PreviewHealthTimeout) 190 if err != nil { 191 return fmt.Errorf("Invalid duration format %s for option --%s: %s", o.Timeout, optionPreviewHealthTimeout, err) 192 } 193 } 194 195 log.Logger().Info("Creating a preview") 196 /* 197 args := o.Args 198 if len(args) > 0 && o.Name == "" { 199 o.Name = args[0] 200 } 201 */ 202 jxClient, currentNs, err := o.JXClient() 203 if err != nil { 204 return err 205 } 206 kubeClient, err := o.KubeClient() 207 if err != nil { 208 return err 209 } 210 kserveClient, _, err := o.KnativeServeClient() 211 if err != nil { 212 return err 213 } 214 apisClient, err := o.ApiExtensionsClient() 215 if err != nil { 216 return err 217 } 218 err = kube.RegisterEnvironmentCRD(apisClient) 219 if err != nil { 220 return err 221 } 222 err = kube.RegisterGitServiceCRD(apisClient) 223 if err != nil { 224 return err 225 } 226 err = kube.RegisterUserCRD(apisClient) 227 if err != nil { 228 return err 229 } 230 231 ns := o.DevNamespace 232 if ns == "" { 233 ns, _, err = kube.GetDevNamespace(kubeClient, currentNs) 234 if err != nil { 235 return err 236 } 237 } 238 o.DevNamespace = ns 239 240 err = o.DefaultValues(ns, true) 241 if err != nil { 242 return err 243 } 244 245 projectConfig, _, err := config.LoadProjectConfig(o.Dir) 246 if err != nil { 247 return err 248 } 249 250 if o.GitInfo == nil { 251 log.Logger().Warnf("No GitInfo found") 252 } else if o.GitInfo.Organisation == "" { 253 log.Logger().Warnf("No GitInfo.Organisation found") 254 } else if o.GitInfo.Name == "" { 255 log.Logger().Warnf("No GitInfo.Name found") 256 } 257 258 // we need pull request info to include 259 authConfigSvc, err := o.GitAuthConfigService() 260 if err != nil { 261 return err 262 } 263 264 prNum, err := strconv.Atoi(o.PullRequestName) 265 if err != nil { 266 log.Logger().Warnf( 267 "Unable to convert PR " + o.PullRequestName + " to a number") 268 } 269 270 var user *v1.UserSpec 271 buildStatus := "" 272 buildStatusUrl := "" 273 274 var pullRequest *gits.GitPullRequest 275 276 if o.GitInfo != nil { 277 gitKind, err := o.GitServerKind(o.GitInfo) 278 if err != nil { 279 return err 280 } 281 282 ghOwner, err := o.GetGitHubAppOwner(o.GitInfo) 283 if err != nil { 284 return err 285 } 286 gitProvider, err := o.NewGitProvider(o.GitInfo.URL, "message", authConfigSvc, gitKind, ghOwner, o.BatchMode, o.Git()) 287 if err != nil { 288 return fmt.Errorf("cannot create Git provider %v", err) 289 } 290 291 resolver := users.GitUserResolver{ 292 GitProvider: gitProvider, 293 JXClient: jxClient, 294 Namespace: currentNs, 295 } 296 297 if prNum > 0 { 298 pullRequest, err = gitProvider.GetPullRequest(o.GitInfo.Organisation, o.GitInfo, prNum) 299 if err != nil { 300 log.Logger().Warnf("issue getting pull request %s, %s, %v: %v", o.GitInfo.Organisation, o.GitInfo.Name, prNum, err) 301 } 302 commits, err := gitProvider.GetPullRequestCommits(o.GitInfo.Organisation, o.GitInfo, prNum) 303 if err != nil { 304 log.Logger().Warnf( 305 "Unable to get commits: %s", err.Error()) 306 } 307 if pullRequest != nil { 308 prAuthor := pullRequest.Author 309 if prAuthor != nil { 310 author, err := resolver.Resolve(prAuthor) 311 if err != nil { 312 return err 313 } 314 author, err = resolver.UpdateUserFromPRAuthor(author, pullRequest, commits) 315 if err != nil { 316 // This isn't fatal, just nice to have! 317 log.Logger().Warnf("Unable to update user %s from %s because %v", prAuthor.Name, o.PullRequestName, err) 318 } 319 if author != nil { 320 user = &v1.UserSpec{ 321 Username: author.Spec.Login, 322 Name: author.Spec.Name, 323 ImageURL: author.Spec.AvatarURL, 324 LinkURL: author.Spec.URL, 325 } 326 } 327 } 328 } 329 330 statuses, err := gitProvider.ListCommitStatus(o.GitInfo.Organisation, o.GitInfo.Name, pullRequest.LastCommitSha) 331 332 if err != nil { 333 log.Logger().Warnf( 334 "Unable to get statuses for PR %s", o.PullRequestName) 335 } 336 337 if len(statuses) > 0 { 338 status := statuses[len(statuses)-1] 339 buildStatus = status.State 340 buildStatusUrl = status.TargetURL 341 } 342 } 343 } 344 345 if o.ReleaseName == "" { 346 _, noTiller, helmTemplate, err := o.TeamHelmBin() 347 if err != nil { 348 return err 349 } 350 if noTiller || helmTemplate { 351 o.ReleaseName = "preview" 352 } else { 353 o.ReleaseName = o.Namespace 354 } 355 } 356 357 environmentsResource := jxClient.JenkinsV1().Environments(ns) 358 env, err := environmentsResource.Get(o.Name, metav1.GetOptions{}) 359 if err == nil { 360 // lets check for updates... 361 update := false 362 363 spec := &env.Spec 364 source := &spec.Source 365 if spec.Label != o.Label { 366 spec.Label = o.Label 367 update = true 368 } 369 if spec.Namespace != o.Namespace { 370 spec.Namespace = o.Namespace 371 update = true 372 } 373 if spec.Namespace != o.Namespace { 374 spec.Namespace = o.Namespace 375 update = true 376 } 377 if spec.Kind != v1.EnvironmentKindTypePreview { 378 spec.Kind = v1.EnvironmentKindTypePreview 379 update = true 380 } 381 if source.Kind != v1.EnvironmentRepositoryTypeGit { 382 source.Kind = v1.EnvironmentRepositoryTypeGit 383 update = true 384 } 385 if source.URL != o.SourceURL { 386 source.URL = o.SourceURL 387 update = true 388 } 389 if source.Ref != o.SourceRef { 390 source.Ref = o.SourceRef 391 update = true 392 } 393 394 gitSpec := spec.PreviewGitSpec 395 if gitSpec.BuildStatus != buildStatus { 396 gitSpec.BuildStatus = buildStatus 397 update = true 398 } 399 if gitSpec.BuildStatusURL != buildStatusUrl { 400 gitSpec.BuildStatusURL = buildStatusUrl 401 update = true 402 } 403 if gitSpec.ApplicationName != o.Application { 404 gitSpec.ApplicationName = o.Application 405 update = true 406 } 407 if pullRequest != nil { 408 if gitSpec.Title != pullRequest.Title { 409 gitSpec.Title = pullRequest.Title 410 update = true 411 } 412 if gitSpec.Description != pullRequest.Body { 413 gitSpec.Description = pullRequest.Body 414 update = true 415 } 416 } 417 if gitSpec.URL != o.PullRequestURL { 418 gitSpec.URL = o.PullRequestURL 419 update = true 420 } 421 if user != nil { 422 if gitSpec.User.Username != user.Username || 423 gitSpec.User.ImageURL != user.ImageURL || 424 gitSpec.User.Name != user.Name || 425 gitSpec.User.LinkURL != user.LinkURL { 426 gitSpec.User = *user 427 update = true 428 } 429 } 430 431 if update { 432 env, err = environmentsResource.PatchUpdate(env) 433 if err != nil { 434 return fmt.Errorf("Failed to update Environment %s due to %s", o.Name, err) 435 } 436 } 437 } else { 438 // lets create a new preview environment 439 previewGitSpec := v1.PreviewGitSpec{ 440 ApplicationName: o.Application, 441 Name: o.PullRequestName, 442 URL: o.PullRequestURL, 443 BuildStatus: buildStatus, 444 BuildStatusURL: buildStatusUrl, 445 } 446 if pullRequest != nil { 447 previewGitSpec.Title = pullRequest.Title 448 previewGitSpec.Description = pullRequest.Body 449 } 450 if user != nil { 451 previewGitSpec.User = *user 452 } 453 env = &v1.Environment{ 454 ObjectMeta: metav1.ObjectMeta{ 455 Name: o.Name, 456 Annotations: map[string]string{ 457 kube.AnnotationReleaseName: o.ReleaseName, 458 }, 459 }, 460 Spec: v1.EnvironmentSpec{ 461 Namespace: o.Namespace, 462 Label: o.Label, 463 Kind: v1.EnvironmentKindTypePreview, 464 PromotionStrategy: v1.PromotionStrategyTypeAutomatic, 465 PullRequestURL: o.PullRequestURL, 466 Order: 999, 467 Source: v1.EnvironmentRepository{ 468 Kind: v1.EnvironmentRepositoryTypeGit, 469 URL: o.SourceURL, 470 Ref: o.SourceRef, 471 }, 472 PreviewGitSpec: previewGitSpec, 473 }, 474 } 475 _, err = environmentsResource.Create(env) 476 if err != nil { 477 return fmt.Errorf("Failed to create environment in namespace %s due to: %s", ns, err) 478 } 479 log.Logger().Infof("Created environment %s", util.ColorInfo(env.Name)) 480 } 481 482 err = kube.EnsureEnvironmentNamespaceSetup(kubeClient, jxClient, env, ns) 483 if err != nil { 484 return err 485 } 486 487 domain, err := kube.GetCurrentDomain(kubeClient, ns) 488 if err != nil { 489 return err 490 } 491 492 values, err := o.GetPreviewValuesConfig(projectConfig, domain) 493 if err != nil { 494 return err 495 } 496 497 config, err := values.String() 498 if err != nil { 499 return err 500 } 501 502 dir, err := os.Getwd() 503 if err != nil { 504 return err 505 } 506 507 configFileName := filepath.Join(dir, opts.ExtraValuesFile) 508 log.Logger().Infof("%s", config) 509 err = ioutil.WriteFile(configFileName, []byte(config), 0600) 510 if err != nil { 511 return err 512 } 513 514 setValues, setStrings := o.GetEnvChartValues(o.Namespace, env) 515 516 helmOptions := helm.InstallChartOptions{ 517 Chart: ".", 518 ReleaseName: o.ReleaseName, 519 Ns: o.Namespace, 520 SetValues: setValues, 521 SetStrings: setStrings, 522 ValueFiles: []string{configFileName}, 523 Wait: true, 524 } 525 526 // if the preview chart has values.yaml then pass that so we can replace any secrets from vault 527 defaultValuesFileName := filepath.Join(dir, opts.ValuesFile) 528 _, err = ioutil.ReadFile(defaultValuesFileName) 529 if err == nil { 530 helmOptions.ValueFiles = append(helmOptions.ValueFiles, defaultValuesFileName) 531 } 532 533 err = o.InstallChartWithOptions(helmOptions) 534 if err != nil { 535 return err 536 } 537 538 url, appNames, err := o.findPreviewURL(kubeClient, kserveClient) 539 540 if url == "" { 541 log.Logger().Warnf("Could not find the service URL in namespace %s for names %s: %s", o.Namespace, strings.Join(appNames, ", "), err.Error()) 542 } else { 543 writePreviewURL(o, url) 544 } 545 546 comment := fmt.Sprintf(":star: PR built and available in a preview environment **%s**", o.Name) 547 if url != "" { 548 comment += fmt.Sprintf(" [here](%s) ", url) 549 } 550 551 pipeline := o.GetJenkinsJobName() 552 build := builds.GetBuildNumber() 553 554 if url != "" || o.PullRequestURL != "" { 555 if pipeline != "" && build != "" { 556 name := naming.ToValidName(pipeline + "-" + build) 557 // lets see if we can update the pipeline 558 activities := jxClient.JenkinsV1().PipelineActivities(ns) 559 key := &kube.PromoteStepActivityKey{ 560 PipelineActivityKey: kube.PipelineActivityKey{ 561 Name: name, 562 Pipeline: pipeline, 563 Build: build, 564 GitInfo: &gits.GitRepository{ 565 Name: o.GitInfo.Name, 566 Organisation: o.GitInfo.Organisation, 567 }, 568 }, 569 } 570 jxClient, _, err = o.JXClient() 571 if err != nil { 572 return err 573 } 574 a, _, p, _, err := key.GetOrCreatePreview(jxClient, ns) 575 if err == nil && a != nil && p != nil { 576 updated := false 577 if p.ApplicationURL == "" { 578 p.ApplicationURL = url 579 updated = true 580 } 581 if p.PullRequestURL == "" && o.PullRequestURL != "" { 582 p.PullRequestURL = o.PullRequestURL 583 updated = true 584 } 585 if updated { 586 _, err = activities.PatchUpdate(a) 587 if err != nil { 588 log.Logger().Warnf("Failed to update PipelineActivities %s: %s", name, err) 589 } else { 590 log.Logger().Infof("Updating PipelineActivities %s which has status %s", name, string(a.Spec.Status)) 591 } 592 } 593 } 594 } else { 595 log.Logger().Warnf("No pipeline and build number available on $JOB_NAME and $BUILD_NUMBER so cannot update PipelineActivities with the preview URLs") 596 } 597 } 598 if !o.SkipAvailabilityCheck && url != "" { 599 // Wait for a 200 range status code, 401 or 404 to make sure that the DNS has propagated 600 f := func() error { 601 resp, err := http.Get(url) // #nosec 602 if err != nil { 603 return errors.Errorf("preview application %s not available, error was %v", url, err) 604 } 605 // 200 - 299 : successful for most types of applications 606 // 401 : an application requiring authentication 607 // 404 : return code for an application where the domain resolves but the root path is not found 608 // 403 : forbidden, the client may not use the same credentials later, default return code for sprint-security 609 if resp.StatusCode < 200 || (resp.StatusCode >= 300 && resp.StatusCode != 401 && resp.StatusCode != 403 && resp.StatusCode != 404) { 610 return errors.Errorf("preview application %s not available, error was %d %s", url, resp.StatusCode, resp.Status) 611 } 612 return nil 613 } 614 notify := func(err error, d time.Duration) { 615 log.Logger().Warnf("%v, delaying for: %v", err, d) 616 } 617 618 exponentialBackOff := backoff.NewExponentialBackOff() 619 exponentialBackOff.InitialInterval = 1 * time.Second 620 exponentialBackOff.MaxInterval = 1 * time.Minute 621 exponentialBackOff.MaxElapsedTime = o.PreviewHealthTimeoutDuration 622 exponentialBackOff.Reset() 623 err := backoff.RetryNotify(f, exponentialBackOff, notify) 624 if err != nil { 625 return errors.Wrapf(err, "error checking if preview application %s is available", url) 626 } 627 628 env, err = environmentsResource.Get(o.Name, metav1.GetOptions{}) 629 if err != nil { 630 return err 631 } 632 if env != nil && env.Spec.PreviewGitSpec.ApplicationURL == "" { 633 env.Spec.PreviewGitSpec.ApplicationURL = url 634 _, err = environmentsResource.PatchUpdate(env) 635 if err != nil { 636 return fmt.Errorf("Failed to update Environment %s due to %s", o.Name, err) 637 } 638 } 639 log.Logger().Infof("Preview application is now available at: %s\n", util.ColorInfo(url)) 640 } 641 642 shouldSentPullRequestComment := true 643 if o.NoComment { 644 shouldSentPullRequestComment = false 645 } 646 if _, ok := env.Annotations[annotationPullRequestCommentSent]; ok && o.SingleComment { 647 shouldSentPullRequestComment = false 648 } 649 if shouldSentPullRequestComment { 650 stepPRCommentOptions := pr.StepPRCommentOptions{ 651 Flags: pr.StepPRCommentFlags{ 652 Owner: o.GitInfo.Organisation, 653 Repository: o.GitInfo.Name, 654 Comment: comment, 655 PR: o.PullRequestName, 656 }, 657 StepPROptions: pr.StepPROptions{ 658 StepOptions: step.StepOptions{ 659 CommonOptions: o.CommonOptions, 660 }, 661 }, 662 } 663 stepPRCommentOptions.BatchMode = true 664 err = stepPRCommentOptions.Run() 665 if err != nil { 666 log.Logger().Warnf("Failed to comment on the Pull Request with owner %s repo %s: %s", o.GitInfo.Organisation, o.GitInfo.Name, err) 667 } else { 668 env, err = environmentsResource.Get(o.Name, metav1.GetOptions{}) 669 if err != nil { 670 return err 671 } 672 if env.Annotations == nil { 673 env.Annotations = map[string]string{} 674 } 675 env.Annotations[annotationPullRequestCommentSent] = time.Now().Format(time.RFC3339) 676 _, err = environmentsResource.PatchUpdate(env) 677 if err != nil { 678 return fmt.Errorf("Failed to update Environment %s due to %s", o.Name, err) 679 } 680 } 681 } 682 683 return o.RunPostPreviewSteps(kubeClient, o.Namespace, url, pipeline, build, o.Application) 684 } 685 686 // findPreviewURL finds the preview URL 687 func (o *PreviewOptions) findPreviewURL(kubeClient kubernetes.Interface, kserveClient kserve.Interface) (string, []string, error) { 688 app := naming.ToValidName(o.Application) 689 appNames := []string{app, o.ReleaseName, o.Namespace + "-preview", o.ReleaseName + "-" + app} 690 url := "" 691 var err error 692 fn := func() (bool, error) { 693 for _, n := range appNames { 694 url, _ = services.FindServiceURL(kubeClient, o.Namespace, n) 695 if url == "" { 696 url, _, err = kserving.FindServiceURL(kserveClient, kubeClient, o.Namespace, n) 697 } 698 if url != "" { 699 err = nil 700 return true, nil 701 } 702 } 703 return false, nil 704 } 705 err = o.RetryUntilTrueOrTimeout(time.Minute, time.Second*5, fn) 706 if err != nil { 707 return "", nil, err 708 } 709 return url, appNames, err 710 } 711 712 // RunPostPreviewSteps lets run any post-preview steps that are configured for all apps in a team 713 func (o *PreviewOptions) RunPostPreviewSteps(kubeClient kubernetes.Interface, ns string, url string, pipeline string, build string, application string) error { 714 teamSettings, err := o.TeamSettings() 715 if err != nil { 716 return err 717 } 718 719 scheme, port, err := services.FindServiceSchemePort(kubeClient, ns, naming.ToValidName(application)) 720 if err != nil { 721 log.Logger().Warnf("Failed to find the service %s : %s", application, err) 722 } 723 internalURL := "" 724 if !(scheme == "" || port == "") { 725 internalURL = scheme + "://" + application + ":" + port // The service URL that is visible within the namespace scope 726 } 727 preferredURL := url 728 if url == "" { 729 preferredURL = internalURL // Set to external URL if an ingress was found, otherwise use the internal URL 730 } 731 732 envVars := map[string]string{ 733 "JX_PREVIEW_URL": preferredURL, 734 "JX_EXTERNAL_URL": url, 735 "JX_INTERNAL_URL": internalURL, 736 "JX_APPLICATION_NAME": application, 737 "JX_SCHEME": scheme, 738 "JX_PORT": port, 739 "JX_PIPELINE": pipeline, 740 "JX_BUILD": build, 741 } 742 743 // Note that post preview jobs need to allow for use cases where no HTTP-based services are published by a pod 744 745 // Post preview jobs should validate input and behave appropriately. Needs a selector to invoke only relevant PPJs? 746 747 jobs := teamSettings.PostPreviewJobs 748 jobResources := kubeClient.BatchV1().Jobs(ns) 749 createdJobs := []*batchv1.Job{} 750 for _, j := range jobs { 751 job := j 752 // TODO lets modify the job name? 753 job2 := o.modifyJob(&job, envVars) 754 log.Logger().Infof("Triggering post preview Job %s in namespace %s", util.ColorInfo(job2.Name), util.ColorInfo(ns)) 755 756 gracePeriod := int64(0) 757 propationPolicy := metav1.DeletePropagationForeground 758 759 // lets try delete it if it exists 760 err = jobResources.Delete(job2.Name, &metav1.DeleteOptions{ 761 GracePeriodSeconds: &gracePeriod, 762 PropagationPolicy: &propationPolicy, 763 }) 764 if err != nil { 765 return err 766 } 767 768 // lets wait for the resource to be gone 769 hasJob := func() (bool, error) { 770 job, err := jobResources.Get(job.Name, metav1.GetOptions{}) 771 return job == nil || err != nil, nil 772 } 773 err = o.RetryUntilTrueOrTimeout(time.Minute, time.Second, hasJob) 774 if err != nil { 775 return err 776 } 777 778 createdJob, err := jobResources.Create(job2) 779 if err != nil { 780 return err 781 } 782 createdJobs = append(createdJobs, createdJob) 783 } 784 return o.waitForJobsToComplete(kubeClient, createdJobs) 785 } 786 787 func (o *PreviewOptions) waitForJobsToComplete(kubeClient kubernetes.Interface, jobs []*batchv1.Job) error { 788 for _, job := range jobs { 789 err := o.waitForJob(kubeClient, job) 790 if err != nil { 791 return err 792 } 793 } 794 return nil 795 } 796 797 // waits for this job to complete 798 func (o *PreviewOptions) waitForJob(kubeClient kubernetes.Interface, job *batchv1.Job) error { 799 name := job.Name 800 ns := job.Namespace 801 log.Logger().Infof("waiting for Job %s in namespace %s to complete...\n", util.ColorInfo(name), util.ColorInfo(ns)) 802 803 count := 0 804 fn := func() (bool, error) { 805 curJob, err := kubeClient.BatchV1().Jobs(ns).Get(name, metav1.GetOptions{}) 806 if err != nil { 807 return true, err 808 } 809 if kube.IsJobFinished(curJob) { 810 if kube.IsJobSucceeded(curJob) { 811 return true, nil 812 } else { 813 failed := curJob.Status.Failed 814 succeeded := curJob.Status.Succeeded 815 return true, fmt.Errorf("Job %s in namepace %s has %d failed containers and %d succeeded containers", name, ns, failed, succeeded) 816 } 817 } 818 count += 1 819 if count > 1 { 820 // TODO we could maybe do better - using a prefix on all logs maybe with the job name? 821 err = o.RunCommandVerbose("kubectl", "logs", "-f", "job/"+name, "-n", ns) 822 if err != nil { 823 return false, err 824 } 825 } 826 return false, nil 827 } 828 err := o.RetryUntilTrueOrTimeout(o.PostPreviewJobTimeoutDuration, o.PostPreviewJobPollDuration, fn) 829 if err != nil { 830 log.Logger().Warnf("\nFailed to complete post Preview Job %s in namespace %s: %s", name, ns, err) 831 } 832 return err 833 } 834 835 // modifyJob adds the given environment variables into all the containers in the job 836 func (o *PreviewOptions) modifyJob(originalJob *batchv1.Job, envVars map[string]string) *batchv1.Job { 837 job := *originalJob 838 for k, v := range envVars { 839 templateSpec := &job.Spec.Template.Spec 840 for i := range templateSpec.Containers { 841 container := &templateSpec.Containers[i] 842 if kube.GetEnvVar(container, k) == nil { 843 container.Env = append(container.Env, corev1.EnvVar{ 844 Name: k, 845 Value: v, 846 }) 847 } 848 } 849 } 850 851 return &job 852 } 853 854 func (o *PreviewOptions) DefaultValues(ns string, warnMissingName bool) error { 855 var err error 856 if o.Application == "" { 857 o.Application, err = o.DiscoverAppName() 858 if err != nil { 859 return err 860 } 861 } 862 863 // fill in default values 864 if o.SourceURL == "" { 865 o.SourceURL = os.Getenv("SOURCE_URL") 866 if o.SourceURL == "" { 867 // Relevant in a Jenkins pipeline triggered by a PR 868 o.SourceURL = os.Getenv("CHANGE_URL") 869 if o.SourceURL == "" { 870 // lets discover the git dir 871 if o.Dir == "" { 872 dir, err := os.Getwd() 873 if err != nil { 874 return err 875 } 876 o.Dir = dir 877 } 878 root, gitConf, err := o.Git().FindGitConfigDir(o.Dir) 879 if err != nil { 880 log.Logger().Warnf("Could not find a .git directory: %s", err) 881 } else { 882 if root != "" { 883 o.Dir = root 884 o.SourceURL, err = o.DiscoverGitURL(gitConf) 885 if err != nil { 886 log.Logger().Warnf("Could not find the remote git source URL: %s", err) 887 } else { 888 if o.SourceRef == "" { 889 o.SourceRef, err = o.Git().Branch(root) 890 if err != nil { 891 log.Logger().Warnf("Could not find the remote git source ref: %s", err) 892 } 893 894 } 895 } 896 } 897 } 898 } 899 } 900 } 901 902 if o.SourceURL == "" { 903 return fmt.Errorf("No sourceURL could be defaulted for the Preview Environment. Use --dir flag to detect the git source URL") 904 } 905 906 if o.PullRequest == "" { 907 o.PullRequest = os.Getenv(util.EnvVarBranchName) 908 } 909 910 o.PullRequestName = strings.TrimPrefix(o.PullRequest, "PR-") 911 912 if o.SourceURL != "" { 913 o.GitInfo, err = gits.ParseGitURL(o.SourceURL) 914 if err != nil { 915 log.Logger().Warnf("Could not parse the git URL %s due to %s", o.SourceURL, err) 916 } else { 917 gitKind, _ := o.GitServerKind(o.GitInfo) 918 o.SourceURL = gits.HttpCloneURL(o.GitInfo, gitKind) 919 if o.PullRequestURL == "" { 920 if o.PullRequest == "" { 921 if warnMissingName { 922 log.Logger().Warnf("No Pull Request name or URL specified nor could one be found via $BRANCH_NAME") 923 } 924 } else { 925 o.PullRequestURL = o.GitInfo.PullRequestURL(o.PullRequestName) 926 } 927 } 928 if o.Name == "" && o.PullRequestName != "" { 929 o.Name = o.GitInfo.Organisation + "-" + o.GitInfo.Name + "-pr-" + o.PullRequestName 930 } 931 if o.Label == "" { 932 o.Label = o.GitInfo.Organisation + "/" + o.GitInfo.Name + " PR-" + o.PullRequestName 933 } 934 } 935 } 936 o.Name = naming.ToValidName(o.Name) 937 if o.Name == "" { 938 return fmt.Errorf("No name could be defaulted for the Preview Environment. Please supply one!") 939 } 940 if o.Namespace == "" { 941 prefix := ns + "-" 942 if len(prefix) > 63 { 943 return fmt.Errorf("Team namespace prefix is too long to create previews %s is too long. Must be no more than 60 character", prefix) 944 } 945 946 o.Namespace = prefix + o.Name 947 if len(o.Namespace) > 63 { 948 max := 62 - len(prefix) 949 size := len(o.Name) 950 951 o.Namespace = prefix + o.Name[size-max:] 952 log.Logger().Warnf("Due the name of the organsation and repository being too long (%s) we are going to trim it to make the preview namespace: %s", o.Name, o.Namespace) 953 } 954 } 955 if len(o.Namespace) > 63 { 956 return fmt.Errorf("Preview namespace %s is too long. Must be no more than 63 character", o.Namespace) 957 } 958 o.Namespace = naming.ToValidName(o.Namespace) 959 if o.Label == "" { 960 o.Label = o.Name 961 } 962 if o.GitInfo == nil { 963 log.Logger().Warnf("No GitInfo could be found!") 964 } 965 return nil 966 } 967 968 // GetPreviewValuesConfig returns the PreviewValuesConfig to use as extraValues for helm 969 func (o *PreviewOptions) GetPreviewValuesConfig(projectConfig *config.ProjectConfig, domain string) (*config.PreviewValuesConfig, error) { 970 repository, err := o.getImageName(projectConfig) 971 if err != nil { 972 return nil, err 973 } 974 975 tag, err := getImageTag() 976 if err != nil { 977 return nil, err 978 } 979 980 if o.HelmValuesConfig.ExposeController == nil { 981 o.HelmValuesConfig.ExposeController = &config.ExposeController{} 982 } 983 o.HelmValuesConfig.ExposeController.Config.Domain = domain 984 985 values := config.PreviewValuesConfig{ 986 ExposeController: o.HelmValuesConfig.ExposeController, 987 Preview: &config.Preview{ 988 Image: &config.Image{ 989 Repository: repository, 990 Tag: tag, 991 }, 992 }, 993 } 994 return &values, nil 995 } 996 997 func writePreviewURL(o *PreviewOptions, url string) { 998 previewFileName := filepath.Join(o.Dir, ".previewUrl") 999 err := ioutil.WriteFile(previewFileName, []byte(url), 0600) 1000 if err != nil { 1001 log.Logger().Warnf("Unable to write preview file") 1002 } 1003 } 1004 1005 func (o *PreviewOptions) getContainerRegistry(projectConfig *config.ProjectConfig) (string, error) { 1006 teamSettings, err := o.TeamSettings() 1007 if err != nil { 1008 return "", errors.Wrap(err, "could not load team") 1009 } 1010 requirements, err := config.GetRequirementsConfigFromTeamSettings(teamSettings) 1011 if err != nil { 1012 return "", errors.Wrap(err, "could not get requirements from team setting") 1013 } 1014 if requirements != nil { 1015 registryHost := requirements.Cluster.Registry 1016 if registryHost != "" { 1017 return registryHost, nil 1018 } 1019 } 1020 1021 registryHost := os.Getenv(JENKINS_X_DOCKER_REGISTRY_SERVICE_HOST) 1022 registry := "" 1023 if projectConfig != nil { 1024 registry = projectConfig.DockerRegistryHost 1025 } 1026 if registryHost == "" { 1027 } 1028 if registry == "" { 1029 registry = os.Getenv(DOCKER_REGISTRY) 1030 } 1031 if registry != "" { 1032 return registry, nil 1033 } 1034 1035 if registryHost == "" { 1036 return "", fmt.Errorf("no %s environment variable found", JENKINS_X_DOCKER_REGISTRY_SERVICE_HOST) 1037 } 1038 registryPort := os.Getenv(JENKINS_X_DOCKER_REGISTRY_SERVICE_PORT) 1039 if registryPort == "" { 1040 return "", fmt.Errorf("no %s environment variable found", JENKINS_X_DOCKER_REGISTRY_SERVICE_PORT) 1041 } 1042 1043 return fmt.Sprintf("%s:%s", registryHost, registryPort), nil 1044 } 1045 1046 func (o *PreviewOptions) getImageName(projectConfig *config.ProjectConfig) (string, error) { 1047 containerRegistry, err := o.getContainerRegistry(projectConfig) 1048 if err != nil { 1049 return "", err 1050 } 1051 1052 organisation := os.Getenv(ORG) 1053 if organisation == "" { 1054 organisation = os.Getenv(REPO_OWNER) 1055 } 1056 if organisation == "" { 1057 return "", fmt.Errorf("no %s environment variable found", ORG) 1058 } 1059 1060 app := os.Getenv(APP_NAME) 1061 if app == "" { 1062 app = os.Getenv(REPO_NAME) 1063 } 1064 if app == "" { 1065 return "", fmt.Errorf("no %s environment variable found", APP_NAME) 1066 } 1067 1068 dockerRegistryOrg := o.GetDockerRegistryOrg(projectConfig, o.GitInfo) 1069 if dockerRegistryOrg == "" { 1070 dockerRegistryOrg = organisation 1071 } 1072 1073 return fmt.Sprintf("%s/%s/%s", containerRegistry, dockerRegistryOrg, app), nil 1074 } 1075 1076 func getImageTag() (string, error) { 1077 tag := os.Getenv(PREVIEW_VERSION) 1078 if tag == "" { 1079 tag = os.Getenv("VERSION") 1080 } 1081 if tag == "" { 1082 return "", fmt.Errorf("no %s environment variable found", PREVIEW_VERSION) 1083 } 1084 return tag, nil 1085 }