github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/controller/controller_commitstatus.go (about)

     1  package controller
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/olli-ai/jx/v2/pkg/cmd/helper"
    11  	"github.com/olli-ai/jx/v2/pkg/kube/naming"
    12  
    13  	"github.com/olli-ai/jx/v2/pkg/cmd/opts"
    14  	"github.com/olli-ai/jx/v2/pkg/prow/config"
    15  
    16  	"github.com/olli-ai/jx/v2/pkg/gits"
    17  
    18  	"github.com/olli-ai/jx/v2/pkg/prow"
    19  
    20  	"k8s.io/client-go/kubernetes"
    21  
    22  	"github.com/olli-ai/jx/v2/pkg/extensions"
    23  
    24  	"github.com/pkg/errors"
    25  
    26  	"github.com/olli-ai/jx/v2/pkg/builds"
    27  
    28  	corev1 "k8s.io/api/core/v1"
    29  
    30  	jenkinsv1client "github.com/jenkins-x/jx-api/pkg/client/clientset/versioned"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/fields"
    33  
    34  	"k8s.io/client-go/tools/cache"
    35  
    36  	"github.com/jenkins-x/jx-logging/pkg/log"
    37  
    38  	jenkinsv1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    39  
    40  	"github.com/olli-ai/jx/v2/pkg/kube"
    41  
    42  	"github.com/spf13/cobra"
    43  )
    44  
    45  // ControllerCommitStatusOptions the options for the controller
    46  type ControllerCommitStatusOptions struct {
    47  	ControllerOptions
    48  }
    49  
    50  // NewCmdControllerCommitStatus creates a command object for the "create" command
    51  func NewCmdControllerCommitStatus(commonOpts *opts.CommonOptions) *cobra.Command {
    52  	options := &ControllerCommitStatusOptions{
    53  		ControllerOptions: ControllerOptions{
    54  			CommonOptions: commonOpts,
    55  		},
    56  	}
    57  
    58  	cmd := &cobra.Command{
    59  		Use:   "commitstatus",
    60  		Short: "Updates commit status",
    61  		Run: func(cmd *cobra.Command, args []string) {
    62  			options.Cmd = cmd
    63  			options.Args = args
    64  			err := options.Run()
    65  			helper.CheckErr(err)
    66  		},
    67  	}
    68  	return cmd
    69  }
    70  
    71  // Run implements this command
    72  func (o *ControllerCommitStatusOptions) Run() error {
    73  	// Always run in batch mode as a controller is never run interactively
    74  	o.BatchMode = true
    75  
    76  	jxClient, ns, err := o.JXClientAndDevNamespace()
    77  	if err != nil {
    78  		return err
    79  	}
    80  	kubeClient, _, err := o.KubeClientAndDevNamespace()
    81  	if err != nil {
    82  		return err
    83  	}
    84  	apisClient, err := o.ApiExtensionsClient()
    85  	if err != nil {
    86  		return err
    87  	}
    88  	err = kube.RegisterCommitStatusCRD(apisClient)
    89  	if err != nil {
    90  		return err
    91  	}
    92  	err = kube.RegisterPipelineActivityCRD(apisClient)
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	commitstatusListWatch := cache.NewListWatchFromClient(jxClient.JenkinsV1().RESTClient(), "commitstatuses", ns, fields.Everything())
    98  	kube.SortListWatchByName(commitstatusListWatch)
    99  	_, commitstatusController := cache.NewInformer(
   100  		commitstatusListWatch,
   101  		&jenkinsv1.CommitStatus{},
   102  		time.Minute*10,
   103  		cache.ResourceEventHandlerFuncs{
   104  			AddFunc: func(obj interface{}) {
   105  				o.onCommitStatusObj(obj, jxClient, ns)
   106  			},
   107  			UpdateFunc: func(oldObj, newObj interface{}) {
   108  				o.onCommitStatusObj(newObj, jxClient, ns)
   109  			},
   110  			DeleteFunc: func(obj interface{}) {
   111  
   112  			},
   113  		},
   114  	)
   115  	stop := make(chan struct{})
   116  	go commitstatusController.Run(stop)
   117  
   118  	podListWatch := cache.NewListWatchFromClient(kubeClient.CoreV1().RESTClient(), "pods", ns, fields.Everything())
   119  	kube.SortListWatchByName(podListWatch)
   120  	_, podWatch := cache.NewInformer(
   121  		podListWatch,
   122  		&corev1.Pod{},
   123  		time.Minute*10,
   124  		cache.ResourceEventHandlerFuncs{
   125  			AddFunc: func(obj interface{}) {
   126  				o.onPodObj(obj, jxClient, kubeClient, ns)
   127  			},
   128  			UpdateFunc: func(oldObj, newObj interface{}) {
   129  				o.onPodObj(newObj, jxClient, kubeClient, ns)
   130  			},
   131  			DeleteFunc: func(obj interface{}) {
   132  
   133  			},
   134  		},
   135  	)
   136  	stop = make(chan struct{})
   137  	podWatch.Run(stop)
   138  
   139  	if err != nil {
   140  		return err
   141  	}
   142  	return nil
   143  }
   144  
   145  func (o *ControllerCommitStatusOptions) onCommitStatusObj(obj interface{}, jxClient jenkinsv1client.Interface, ns string) {
   146  	check, ok := obj.(*jenkinsv1.CommitStatus)
   147  	if !ok {
   148  		log.Logger().Fatalf("commit status controller: unexpected type %v", obj)
   149  	} else {
   150  		err := o.onCommitStatus(check, jxClient, ns)
   151  		if err != nil {
   152  			log.Logger().Fatalf("commit status controller: %v", err)
   153  		}
   154  	}
   155  }
   156  
   157  func (o *ControllerCommitStatusOptions) onCommitStatus(check *jenkinsv1.CommitStatus, jxClient jenkinsv1client.Interface, ns string) error {
   158  	groupedBySha := make(map[string][]jenkinsv1.CommitStatusDetails, 0)
   159  	for _, v := range check.Spec.Items {
   160  		if _, ok := groupedBySha[v.Commit.SHA]; !ok {
   161  			groupedBySha[v.Commit.SHA] = make([]jenkinsv1.CommitStatusDetails, 0)
   162  		}
   163  		groupedBySha[v.Commit.SHA] = append(groupedBySha[v.Commit.SHA], v)
   164  	}
   165  	for _, vs := range groupedBySha {
   166  		var last jenkinsv1.CommitStatusDetails
   167  		for _, v := range vs {
   168  			lastBuildNumber, err := strconv.Atoi(getBuildNumber(last.PipelineActivity.Name))
   169  			if err != nil {
   170  				return err
   171  			}
   172  			buildNumber, err := strconv.Atoi(getBuildNumber(v.PipelineActivity.Name))
   173  			if err != nil {
   174  				return err
   175  			}
   176  			if lastBuildNumber < buildNumber {
   177  				last = v
   178  			}
   179  		}
   180  		err := o.update(&last, jxClient, ns)
   181  		if err != nil {
   182  			gitProvider, gitRepoInfo, err1 := o.getGitProvider(last.Commit.GitURL)
   183  			if err1 != nil {
   184  				return err1
   185  			}
   186  			_, err1 = extensions.NotifyCommitStatus(last.Commit, "error", "", "Internal Error performing commit status updates", "", last.Context, gitProvider, gitRepoInfo)
   187  			if err1 != nil {
   188  				return err
   189  			}
   190  			return err
   191  		}
   192  	}
   193  	return nil
   194  }
   195  
   196  func (o *ControllerCommitStatusOptions) onPodObj(obj interface{}, jxClient jenkinsv1client.Interface, kubeClient kubernetes.Interface, ns string) {
   197  	check, ok := obj.(*corev1.Pod)
   198  	if !ok {
   199  		log.Logger().Fatalf("pod watcher: unexpected type %v", obj)
   200  	} else {
   201  		err := o.onPod(check, jxClient, kubeClient, ns)
   202  		if err != nil {
   203  			log.Logger().Fatalf("pod watcher: %v", err)
   204  		}
   205  	}
   206  }
   207  
   208  func (o *ControllerCommitStatusOptions) onPod(pod *corev1.Pod, jxClient jenkinsv1client.Interface, kubeClient kubernetes.Interface, ns string) error {
   209  	if pod != nil {
   210  		labels := pod.Labels
   211  		if labels != nil {
   212  			buildName := labels[builds.LabelBuildName]
   213  			if buildName == "" {
   214  				buildName = labels[builds.LabelOldBuildName]
   215  			}
   216  			if buildName == "" {
   217  				buildName = labels[builds.LabelPipelineRunName]
   218  			}
   219  			if buildName != "" {
   220  				org := ""
   221  				repo := ""
   222  				pullRequest := ""
   223  				pullPullSha := ""
   224  				pullBaseSha := ""
   225  				buildNumber := ""
   226  				jxBuildNumber := ""
   227  				buildId := ""
   228  				sourceUrl := ""
   229  				branch := ""
   230  
   231  				containers, _, _ := kube.GetContainersWithStatusAndIsInit(pod)
   232  				for _, container := range containers {
   233  					for _, e := range container.Env {
   234  						switch e.Name {
   235  						case "REPO_OWNER":
   236  							org = e.Value
   237  						case "REPO_NAME":
   238  							repo = e.Value
   239  						case "PULL_NUMBER":
   240  							pullRequest = fmt.Sprintf("PR-%s", e.Value)
   241  						case "PULL_PULL_SHA":
   242  							pullPullSha = e.Value
   243  						case "PULL_BASE_SHA":
   244  							pullBaseSha = e.Value
   245  						case "JX_BUILD_NUMBER":
   246  							jxBuildNumber = e.Value
   247  						case "BUILD_NUMBER":
   248  							buildNumber = e.Value
   249  						case "BUILD_ID":
   250  							buildId = e.Value
   251  						case "SOURCE_URL":
   252  							sourceUrl = e.Value
   253  						case "PULL_BASE_REF":
   254  							branch = e.Value
   255  						}
   256  					}
   257  				}
   258  
   259  				sha := pullBaseSha
   260  				if pullRequest == "PR-" {
   261  					pullRequest = ""
   262  				} else {
   263  					sha = pullPullSha
   264  					branch = pullRequest
   265  				}
   266  
   267  				// if BUILD_ID is set, use it, otherwise if JX_BUILD_NUMBER is set, use it, otherwise use BUILD_NUMBER
   268  				if jxBuildNumber != "" {
   269  					buildNumber = jxBuildNumber
   270  				}
   271  				if buildId != "" {
   272  					buildNumber = buildId
   273  				}
   274  
   275  				pipelineActName := naming.ToValidName(fmt.Sprintf("%s-%s-%s-%s", org, repo, branch, buildNumber))
   276  
   277  				// PLM TODO This is a bit of hack, we need a working build controller
   278  				// Try to add the lastCommitSha and gitUrl to the PipelineActivity
   279  				act, err := jxClient.JenkinsV1().PipelineActivities(ns).Get(pipelineActName, metav1.GetOptions{})
   280  				if err != nil {
   281  					// An error just means the activity doesn't exist yet
   282  					log.Logger().Debugf("pod watcher: Unable to find PipelineActivity for %s", pipelineActName)
   283  				} else {
   284  					act.Spec.LastCommitSHA = sha
   285  					act.Spec.GitURL = sourceUrl
   286  					act.Spec.GitOwner = org
   287  					log.Logger().Debugf("pod watcher: Adding lastCommitSha: %s and gitUrl: %s to %s", act.Spec.LastCommitSHA, act.Spec.GitURL, pipelineActName)
   288  					_, err := jxClient.JenkinsV1().PipelineActivities(ns).PatchUpdate(act)
   289  					if err != nil {
   290  						// We can safely return this error as it will just get logged
   291  						return err
   292  					}
   293  				}
   294  				if org != "" && repo != "" && buildNumber != "" && (pullBaseSha != "" || pullPullSha != "") {
   295  					log.Logger().Debugf("pod watcher: build pod: %s, org: %s, repo: %s, buildNumber: %s, pullBaseSha: %s, pullPullSha: %s, pullRequest: %s, sourceUrl: %s", pod.Name, org, repo, buildNumber, pullBaseSha, pullPullSha, pullRequest, sourceUrl)
   296  					if sha == "" {
   297  						log.Logger().Warnf("pod watcher: No sha on %s, not upserting commit status", pod.Name)
   298  					} else {
   299  						prow := prow.Options{
   300  							KubeClient: kubeClient,
   301  							NS:         ns,
   302  						}
   303  						prowConfig, _, err := prow.GetProwConfig()
   304  						if err != nil {
   305  							return errors.Wrap(err, "getting prow config")
   306  						}
   307  						contexts, err := config.GetBranchProtectionContexts(org, repo, prowConfig)
   308  						if err != nil {
   309  							return err
   310  						}
   311  						log.Logger().Debugf("pod watcher: Using contexts %v", contexts)
   312  
   313  						for _, ctx := range contexts {
   314  							if pullRequest != "" {
   315  								name := naming.ToValidName(fmt.Sprintf("%s-%s-%s-%s", org, repo, branch, ctx))
   316  
   317  								err = o.UpsertCommitStatusCheck(name, pipelineActName, sourceUrl, sha, pullRequest, ctx, pod.Status.Phase, jxClient, ns)
   318  								if err != nil {
   319  									return err
   320  								}
   321  							}
   322  						}
   323  					}
   324  				}
   325  			}
   326  
   327  		}
   328  
   329  	}
   330  	return nil
   331  }
   332  
   333  func (o *ControllerCommitStatusOptions) UpsertCommitStatusCheck(name string, pipelineActName string, url string, sha string, pullRequest string, context string, phase corev1.PodPhase, jxClient jenkinsv1client.Interface, ns string) error {
   334  	if name != "" {
   335  
   336  		status, err := jxClient.JenkinsV1().CommitStatuses(ns).Get(name, metav1.GetOptions{})
   337  		create := false
   338  		insert := false
   339  		actRef := jenkinsv1.ResourceReference{}
   340  		if err != nil {
   341  			create = true
   342  		} else {
   343  			log.Logger().Infof("pod watcher: commit status already exists for %s", name)
   344  		}
   345  		// Create the activity reference
   346  		act, err := jxClient.JenkinsV1().PipelineActivities(ns).Get(pipelineActName, metav1.GetOptions{})
   347  		if err == nil {
   348  			actRef.Name = act.Name
   349  			actRef.Kind = act.Kind
   350  			actRef.UID = act.UID
   351  			actRef.APIVersion = act.APIVersion
   352  		}
   353  
   354  		possibleStatusDetails := make([]int, 0)
   355  		for i, v := range status.Spec.Items {
   356  			if v.Commit.SHA == sha && v.PipelineActivity.Name == pipelineActName {
   357  				possibleStatusDetails = append(possibleStatusDetails, i)
   358  			}
   359  		}
   360  		statusDetails := jenkinsv1.CommitStatusDetails{}
   361  		log.Logger().Debugf("pod watcher: Discovered possible status details %v", possibleStatusDetails)
   362  		if len(possibleStatusDetails) == 1 {
   363  			log.Logger().Debugf("CommitStatus %s for pipeline %s already exists", name, pipelineActName)
   364  		} else if len(possibleStatusDetails) == 0 {
   365  			insert = true
   366  		} else {
   367  			return fmt.Errorf("More than %d status detail for sha %s, should 1 or 0, found %v", len(possibleStatusDetails), sha, possibleStatusDetails)
   368  		}
   369  
   370  		if create || insert {
   371  			// This is not the same pipeline activity the status was created for,
   372  			// or there is no existing status, so we make a new one
   373  			statusDetails = jenkinsv1.CommitStatusDetails{
   374  				Checked: false,
   375  				Commit: jenkinsv1.CommitStatusCommitReference{
   376  					GitURL:      url,
   377  					PullRequest: pullRequest,
   378  					SHA:         sha,
   379  				},
   380  				PipelineActivity: actRef,
   381  				Context:          context,
   382  			}
   383  		}
   384  		if create {
   385  			log.Logger().Infof("pod watcher: Creating commit status for pipeline activity %s", pipelineActName)
   386  			status = &jenkinsv1.CommitStatus{
   387  				ObjectMeta: metav1.ObjectMeta{
   388  					Name: name,
   389  					Labels: map[string]string{
   390  						"lastCommitSha": sha,
   391  					},
   392  				},
   393  				Spec: jenkinsv1.CommitStatusSpec{
   394  					Items: []jenkinsv1.CommitStatusDetails{
   395  						statusDetails,
   396  					},
   397  				},
   398  			}
   399  			_, err := jxClient.JenkinsV1().CommitStatuses(ns).Create(status)
   400  			if err != nil {
   401  				return err
   402  			}
   403  
   404  		} else if insert {
   405  			status.Spec.Items = append(status.Spec.Items, statusDetails)
   406  			log.Logger().Infof("pod watcher: Adding commit status for pipeline activity %s", pipelineActName)
   407  			_, err := jxClient.JenkinsV1().CommitStatuses(ns).PatchUpdate(status)
   408  			if err != nil {
   409  				return err
   410  			}
   411  		} else {
   412  			log.Logger().Debugf("pod watcher: Not updating or creating pipeline activity %s", pipelineActName)
   413  		}
   414  	} else {
   415  		return errors.New("commit status controller: Must supply name")
   416  	}
   417  	return nil
   418  }
   419  
   420  func (o *ControllerCommitStatusOptions) update(statusDetails *jenkinsv1.CommitStatusDetails, jxClient jenkinsv1client.Interface, ns string) error {
   421  	gitProvider, gitRepoInfo, err := o.getGitProvider(statusDetails.Commit.GitURL)
   422  	if err != nil {
   423  		return err
   424  	}
   425  	pass := false
   426  	if statusDetails.Checked {
   427  		var commentBuilder strings.Builder
   428  		pass = true
   429  		for _, c := range statusDetails.Items {
   430  			if !c.Pass {
   431  				pass = false
   432  				fmt.Fprintf(&commentBuilder, "%s | %s | %s | TODO | `/test this`\n", c.Name, c.Description, statusDetails.Commit.SHA)
   433  			}
   434  		}
   435  		if pass {
   436  			_, err := extensions.NotifyCommitStatus(statusDetails.Commit, "success", "", "Completed successfully", "", statusDetails.Context, gitProvider, gitRepoInfo)
   437  			if err != nil {
   438  				return err
   439  			}
   440  		} else {
   441  			comment := fmt.Sprintf(
   442  				"The following commit statusDetails checks **failed**, say `/retest` to rerun them all:\n"+
   443  					"\n"+
   444  					"Name | Description | Commit | Details | Rerun command\n"+
   445  					"--- | --- | --- | --- | --- \n"+
   446  					"%s\n"+
   447  					"<details>\n"+
   448  					"\n"+
   449  					"Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).\n"+
   450  					"</details>", commentBuilder.String())
   451  			_, err := extensions.NotifyCommitStatus(statusDetails.Commit, "failure", "", fmt.Sprintf("%s failed", statusDetails.Context), comment, statusDetails.Context, gitProvider, gitRepoInfo)
   452  			if err != nil {
   453  				return err
   454  			}
   455  		}
   456  	} else {
   457  		_, err = extensions.NotifyCommitStatus(statusDetails.Commit, "pending", "", fmt.Sprintf("Waiting for %s to complete", statusDetails.Context), "", statusDetails.Context, gitProvider, gitRepoInfo)
   458  		if err != nil {
   459  			return err
   460  		}
   461  	}
   462  	return nil
   463  }
   464  
   465  func (o *ControllerCommitStatusOptions) getGitProvider(url string) (gits.GitProvider, *gits.GitRepository, error) {
   466  	// TODO This is an epic hack to get the git stuff working
   467  	gitInfo, err := gits.ParseGitURL(url)
   468  	if err != nil {
   469  		return nil, nil, err
   470  	}
   471  	authConfigSvc, err := o.GitAuthConfigService()
   472  	if err != nil {
   473  		return nil, nil, err
   474  	}
   475  	gitKind, err := o.GitServerKind(gitInfo)
   476  	if err != nil {
   477  		return nil, nil, err
   478  	}
   479  	for _, server := range authConfigSvc.Config().Servers {
   480  		if server.Kind == gitKind && len(server.Users) >= 1 {
   481  			// Just grab the first user for now
   482  			username := server.Users[0].Username
   483  			apiToken := server.Users[0].ApiToken
   484  			err = os.Setenv("GIT_USERNAME", username)
   485  			if err != nil {
   486  				return nil, nil, err
   487  			}
   488  			err = os.Setenv("GIT_API_TOKEN", apiToken)
   489  			if err != nil {
   490  				return nil, nil, err
   491  			}
   492  			break
   493  		}
   494  	}
   495  	return o.CreateGitProviderForURLWithoutKind(url)
   496  }
   497  
   498  func getBuildNumber(pipelineActName string) string {
   499  	if pipelineActName == "" {
   500  		return "-1"
   501  	}
   502  	pipelineParts := strings.Split(pipelineActName, "-")
   503  	if len(pipelineParts) > 3 {
   504  		return pipelineParts[len(pipelineParts)-1]
   505  	} else {
   506  		return ""
   507  	}
   508  
   509  }