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  }