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 }