github.com/jenkins-x/jx/v2@v2.1.155/pkg/tekton/pipeline_info.go (about) 1 package tekton 2 3 import ( 4 "fmt" 5 "regexp" 6 "sort" 7 "strconv" 8 "strings" 9 "time" 10 11 v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1" 12 "github.com/jenkins-x/jx-logging/pkg/log" 13 "github.com/jenkins-x/jx/v2/pkg/builds" 14 "github.com/jenkins-x/jx/v2/pkg/gits" 15 "github.com/jenkins-x/jx/v2/pkg/kube" 16 "github.com/jenkins-x/jx/v2/pkg/tekton/syntax" 17 "github.com/jenkins-x/jx/v2/pkg/util" 18 "github.com/pkg/errors" 19 "github.com/tektoncd/pipeline/pkg/apis/pipeline" 20 tektonv1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" 21 corev1 "k8s.io/api/core/v1" 22 knativeapis "knative.dev/pkg/apis" 23 ) 24 25 // PipelineRunInfo provides information on a PipelineRun and its stages for use in getting logs and populating activity 26 type PipelineRunInfo struct { 27 Name string 28 Organisation string 29 Repository string 30 Branch string 31 Context string 32 Build string 33 BuildNumber int 34 Pipeline string 35 PipelineRun string 36 LastCommitSHA string 37 BaseSHA string 38 LastCommitMessage string 39 LastCommitURL string 40 GitURL string 41 GitInfo *gits.GitRepository 42 Stages []*StageInfo 43 Type string 44 CreatedTime time.Time 45 } 46 47 // StageInfo provides information on a particular stage, including its pod info or info on its nested stages 48 type StageInfo struct { 49 // TODO: For now, we're not including git info - we're going to assume we have the same git info for the whole 50 // pipeline. 51 Name string 52 53 // These fields will populated for all non-parent stages 54 PodName string 55 Task string 56 TaskRun string 57 FirstStepImage string 58 CreatedTime time.Time 59 Pod *corev1.Pod 60 61 // These fields will only be populated for appropriate parent stages 62 Parallel []*StageInfo 63 Stages []*StageInfo 64 65 // This field will be non-empty if this is a nested stage, containing a list of the names of all its parent stages with the top-level parent first 66 Parents []string 67 } 68 69 // GetStageNameIncludingParents constructs a full stage name including its parents, if they exist. 70 func (si *StageInfo) GetStageNameIncludingParents() string { 71 if si.Name != "" { 72 return strings.NewReplacer("-", " ").Replace(strings.Join(append(si.Parents, si.Name), " / ")) 73 } 74 return si.PodName 75 } 76 77 // PipelineRunInfoFilter allows specifying criteria on which to filter a list of PipelineRunInfos 78 type PipelineRunInfoFilter struct { 79 Owner string 80 Repository string 81 Branch string 82 Build string 83 Filter string 84 Pending bool 85 Context string 86 } 87 88 // GetBuild gets the build identifier 89 func (pri PipelineRunInfo) GetBuild() string { 90 return pri.Build 91 } 92 93 // GetOrderedTaskStages gets all the stages in this pipeline which actually contain a Task, in rough execution order 94 // TODO: Handle parallelism better, where execution is not a straight line. 95 func (pri *PipelineRunInfo) GetOrderedTaskStages() []*StageInfo { 96 var stages []*StageInfo 97 98 for _, n := range pri.Stages { 99 stages = append(stages, n.getOrderedTaskStagesForStage()...) 100 } 101 102 return stages 103 } 104 105 func (si *StageInfo) getOrderedTaskStagesForStage() []*StageInfo { 106 // If this is a Task Stage, not a parent Stage, return itself 107 if si.Task != "" { 108 return []*StageInfo{si} 109 } 110 111 var stages []*StageInfo 112 113 if len(si.Stages) > 0 { 114 for _, n := range si.Stages { 115 stages = append(stages, n.getOrderedTaskStagesForStage()...) 116 } 117 } 118 119 if len(si.Parallel) > 0 { 120 for _, n := range si.Parallel { 121 stages = append(stages, n.getOrderedTaskStagesForStage()...) 122 } 123 } 124 125 return stages 126 } 127 128 // CreatePipelineRunInfo looks up the PipelineRun for a given name and creates the PipelineRunInfo for it 129 func CreatePipelineRunInfo(prName string, podList *corev1.PodList, ps *v1.PipelineStructure, pr *tektonv1alpha1.PipelineRun) (*PipelineRunInfo, error) { 130 branch := "" 131 lastCommitSha := "" 132 lastCommitMessage := "" 133 lastCommitURL := "" 134 owner := "" 135 repo := "" 136 build := "" 137 pullRefs := "" 138 pullBaseSha := "" 139 pullPullSha := "" 140 shaFromGitInit := "" 141 shaRegexp, err := regexp.Compile("\b[a-z0-9]{40}\b") 142 if err != nil { 143 log.Logger().Warnf("Failed to compile regexp because %s", err) 144 } 145 gitURL := "" 146 147 if pr == nil { 148 return nil, errors.New(fmt.Sprintf("PipelineRun %s cannot be found", prName)) 149 } 150 151 pipelineType := BuildPipeline 152 153 if strings.HasPrefix(pr.Name, MetaPipeline.String()+"-") { 154 pipelineType = MetaPipeline 155 } 156 157 pri := &PipelineRunInfo{ 158 Name: possiblyUniquePipelineResourceName(pr.Labels[LabelOwner], pr.Labels[LabelRepo], pr.Labels[LabelBranch], pr.Labels[LabelContext], pr.Labels[LabelType], false) + "-" + pr.Labels[LabelBuild], 159 PipelineRun: pr.Name, 160 Pipeline: pr.Spec.PipelineRef.Name, 161 Type: pipelineType.String(), 162 CreatedTime: pr.CreationTimestamp.Time, 163 } 164 165 var pod *corev1.Pod 166 167 prStatus := pr.Status.GetCondition(knativeapis.ConditionSucceeded) 168 if err := pri.SetPodsForPipelineRun(podList, ps); err != nil { 169 return nil, errors.Wrapf(err, "Failure populating stages and pods for PipelineRun %s", prName) 170 } 171 172 pod = pri.FindFirstStagePod() 173 174 if pod == nil { 175 if prStatus != nil && prStatus.Status == corev1.ConditionUnknown { 176 return nil, errors.New(fmt.Sprintf("Couldn't find a Stage with steps for PipelineRun %s", prName)) 177 } 178 // Just return nil if the pipeline run is completed and its pods have been GCed 179 return nil, nil 180 } 181 182 if pod.Labels != nil { 183 pri.Context = pod.Labels[LabelContext] 184 } 185 containers, _, isInit := kube.GetContainersWithStatusAndIsInit(pod) 186 for _, c := range containers { 187 container := c 188 // We historically used the git source step automatically injected by Tekton to get the git URL and the sha or 189 // branch being built, but that is no longer going to always be accurate due to our bespoke git merge step 190 // handling checkout/merging of PR branches into the target branch. 191 // The Prow/Lighthouse provided environment variables are a better source of truth, but we preserve this logic 192 // for now in case of edge cases, as a fallback. 193 if strings.HasPrefix(container.Name, "build-step-git-source") || strings.HasPrefix(container.Name, "step-git-source") { 194 _, args := kube.GetCommandAndArgs(&container, isInit) 195 for i := 0; i <= len(args)-2; i += 2 { 196 key := args[i] 197 value := args[i+1] 198 199 switch key { 200 case "-url": 201 gitURL = value 202 case "-revision": 203 if shaRegexp.MatchString(value) { 204 shaFromGitInit = value 205 } else { 206 branch = value 207 } 208 } 209 } 210 } 211 for _, v := range container.Env { 212 if v.Value == "" { 213 continue 214 } 215 // PULL_PULL_SHA is set by Prow/Lighthouse with the HEAD SHA of the PR being built, or the HEAD SHA of the 216 // first PR in a batch. It will be empty for non-PR builds. 217 if v.Name == "PULL_PULL_SHA" { 218 pullPullSha = v.Value 219 } 220 // PULL_BASE_SHA is set by Prow/Lighthouse with the target SHA for a PR build, or with just the HEAD SHA for 221 // master. It is always set. 222 if v.Name == "PULL_BASE_SHA" { 223 pullBaseSha = v.Value 224 } 225 // BRANCH_NAME is set by Prow/Lighthouse with the branch name being built - either PR-123 or master in almost 226 // all cases. It is always set. 227 if v.Name == util.EnvVarBranchName { 228 branch = v.Value 229 } 230 // REPO_OWNER is set by Prow/Lighthouse with the org or user the repo is under on the SCM provider. It is 231 // always set. 232 if v.Name == "REPO_OWNER" { 233 owner = v.Value 234 } 235 // REPO_NAME is set by Prow/Lighthouse with the repo name. It is always set. 236 if v.Name == "REPO_NAME" { 237 repo = v.Value 238 } 239 // Deprecated - this is only set for static masters. 240 if v.Name == "JX_BUILD_NUMBER" { 241 build = v.Value 242 } 243 // SOURCE_URL is set by Prow/Lighthouse with the clone URL for the repo. It is always set. 244 if v.Name == "SOURCE_URL" && gitURL == "" { 245 gitURL = v.Value 246 } 247 // PULL_REFS is set by Prow/Lighthouse with a comma-separated list of colon-delimited pairs of branch:ref 248 // involved in the build. For PRs, it will be "master:...,1:...", for batch builds, it will be "master:...,1:...,2:...", 249 // and for release builds, it will be "master:...". It is always set. 250 if v.Name == "PULL_REFS" && pullRefs == "" { 251 pullRefs = v.Value 252 } 253 } 254 if branch == "" { 255 for _, v := range container.Env { 256 // PULL_BASE_REF is set by Prow/Lighthouse to the branch or ref name the PR is targeting, like "master". 257 // It is only set for PR and batch builds. 258 if v.Name == "PULL_BASE_REF" { 259 build = v.Value 260 } 261 } 262 } 263 if build == "" { 264 for _, v := range container.Env { 265 // BUILD_NUMBER is set by the metapipeline. This used to also look at BUILD_ID, but we don't set that 266 // any more. 267 if v.Name == "BUILD_NUMBER" { 268 build = v.Value 269 } 270 } 271 } 272 } 273 274 if pullBaseSha != "" { 275 pri.BaseSHA = pullBaseSha 276 } 277 278 if pullPullSha != "" { 279 lastCommitSha = pullPullSha 280 } 281 282 // If we have the PULL_REFS env var, fall back on it for the lastCommitSha and base sha. 283 if pullRefs != "" { 284 splitRefs := strings.Split(pullRefs, ",") 285 // If there's at least one entry, use the first entry for the base sha. 286 if len(splitRefs) > 0 { 287 if pri.BaseSHA == "" { 288 pri.BaseSHA = shaFromPullRefEntry(splitRefs[0]) 289 } 290 } 291 // If there are at least two entries, use the second entry for the lastCommitSha 292 if len(splitRefs) > 1 { 293 if lastCommitSha == "" { 294 // If we don't have a lastCommitSha, use the second ref 295 lastCommitSha = shaFromPullRefEntry(splitRefs[1]) 296 } 297 } 298 } 299 300 // Fall back on git checkout if for some reason the last commit sha wasn't set based on env vars. 301 if lastCommitSha == "" { 302 lastCommitSha = shaFromGitInit 303 } 304 305 if build == "" { 306 build = builds.GetBuildNumberFromLabels(pr.Labels) 307 } 308 if build == "" { 309 build = "1" 310 } 311 buildNumber, err := strconv.Atoi(build) 312 if err != nil { 313 buildNumber = 1 314 } 315 316 pri.Build = build 317 pri.BuildNumber = buildNumber 318 pri.Branch = branch 319 if gitURL != "" { 320 gitInfo, err := gits.ParseGitURL(gitURL) 321 if err != nil { 322 return nil, errors.Wrapf(err, "Failed to parse Git URL %s", gitURL) 323 } 324 if owner == "" { 325 owner = gitInfo.Organisation 326 } 327 if repo == "" { 328 repo = gitInfo.Name 329 } 330 pri.GitInfo = gitInfo 331 pri.Pipeline = owner + "/" + repo + "/" + branch 332 pri.Name = owner + "-" + repo + "-" + branch + "-" + build 333 pri.Organisation = owner 334 pri.Repository = repo 335 pri.GitURL = gitURL 336 pri.LastCommitMessage = lastCommitMessage 337 pri.LastCommitSHA = lastCommitSha 338 pri.LastCommitURL = lastCommitURL 339 } 340 return pri, nil 341 } 342 343 // shaFromPullRefEntry returns the sha from "some-ref-name-or-pr-number:01234567890abcdef...", returning an empty string 344 // if the entry isn't in that format. 345 func shaFromPullRefEntry(refEntry string) string { 346 idx := strings.LastIndex(refEntry, ":") 347 if idx > 0 { 348 return refEntry[idx+1:] 349 } 350 return "" 351 } 352 353 // SetPodsForPipelineRun populates the pods for all stages within its PipelineRunInfo 354 func (pri *PipelineRunInfo) SetPodsForPipelineRun(podList *corev1.PodList, ps *v1.PipelineStructure) error { 355 if pri.PipelineRun == "" { 356 return errors.New("No PipelineRun specified") 357 } 358 359 if ps == nil { 360 return errors.New(fmt.Sprintf("Could not find PipelineStructure for PipelineRun %s", pri.PipelineRun)) 361 } 362 363 pscs := ps.GetAllStagesAndChildren() 364 365 var firstTaskStage *StageInfo 366 367 for _, psc := range pscs { 368 pri.Stages = append(pri.Stages, stageAndChildrenToStageInfo(psc, []string{})) 369 } 370 371 for _, si := range pri.Stages { 372 if firstTaskStage == nil { 373 firstTaskStage = si 374 } 375 if err := si.SetPodsForStageInfo(podList, pri.PipelineRun); err != nil { 376 return errors.Wrapf(err, "Couldn't populate Pods for Stages") 377 } 378 } 379 380 return nil 381 } 382 383 // SetPodsForStageInfo populates the pods for a particular stage and/or its children 384 func (si *StageInfo) SetPodsForStageInfo(podList *corev1.PodList, prName string) error { 385 var podListItems []corev1.Pod 386 387 for _, p := range podList.Items { 388 if p.Labels[syntax.LabelStageName] == syntax.MangleToRfc1035Label(si.Name, "") && p.Labels[pipeline.GroupName+pipeline.PipelineRunLabelKey] == prName { 389 podListItems = append(podListItems, p) 390 } 391 } 392 393 if si.Task != "" { 394 if len(podListItems) == 0 { 395 // TODO: Probably the pod just hasn't started yet, so return nil 396 return nil 397 } 398 if len(podListItems) > 1 { 399 return errors.New(fmt.Sprintf("Too many Pods (%d) found for PipelineRun %s and Stage %s", len(podListItems), prName, si.Name)) 400 } 401 pod := podListItems[0] 402 si.PodName = pod.Name 403 si.Task = pod.Labels[builds.LabelTaskName] 404 si.TaskRun = pod.Labels[builds.LabelTaskRunName] 405 si.Pod = &pod 406 si.CreatedTime = pod.CreationTimestamp.Time 407 containers, _, isInit := kube.GetContainersWithStatusAndIsInit(&pod) 408 if isInit && len(containers) > 2 { 409 si.FirstStepImage = containers[2].Image 410 } else if !isInit && len(containers) > 1 { 411 si.FirstStepImage = containers[1].Image 412 } 413 } else if len(si.Stages) > 0 { 414 for _, child := range si.Stages { 415 if err := child.SetPodsForStageInfo(podList, prName); err != nil { 416 return err 417 } 418 } 419 } else if len(si.Parallel) > 0 { 420 for _, child := range si.Parallel { 421 if err := child.SetPodsForStageInfo(podList, prName); err != nil { 422 return err 423 } 424 } 425 } 426 427 return nil 428 } 429 430 // FindFirstStagePod finds the first stage in this pipeline run to have a pod, and then returns its pod 431 func (pri *PipelineRunInfo) FindFirstStagePod() *corev1.Pod { 432 for _, s := range pri.Stages { 433 found := s.findTaskStageInfo() 434 if found != nil { 435 return found.Pod 436 } 437 } 438 return nil 439 } 440 441 // findTaskStageInfo gets the first stage that should actually have a pod created for it 442 func (si *StageInfo) findTaskStageInfo() *StageInfo { 443 if si.Task != "" { 444 return si 445 } 446 for _, s := range si.Parallel { 447 child := s.findTaskStageInfo() 448 if child != nil { 449 return child 450 } 451 } 452 for _, s := range si.Stages { 453 child := s.findTaskStageInfo() 454 if child != nil { 455 return child 456 } 457 } 458 459 return nil 460 } 461 462 // GetFullChildStageNames gets the fully qualified (i.e., with parents appended) names of each stage underneath this one. 463 func (si *StageInfo) GetFullChildStageNames(includeSelf bool) []string { 464 if si.Task != "" && includeSelf { 465 return []string{si.GetStageNameIncludingParents()} 466 } 467 468 var names []string 469 for _, n := range si.Parallel { 470 names = append(names, n.GetFullChildStageNames(true)...) 471 } 472 for _, n := range si.Stages { 473 names = append(names, n.GetFullChildStageNames(true)...) 474 } 475 476 return names 477 } 478 479 func stageAndChildrenToStageInfo(psc *v1.PipelineStageAndChildren, parents []string) *StageInfo { 480 si := &StageInfo{ 481 Name: psc.Stage.Name, 482 Parents: parents, 483 } 484 if psc.Stage.TaskRef != nil { 485 si.Task = *psc.Stage.TaskRef 486 } 487 488 for _, s := range psc.Stages { 489 stage := s 490 si.Stages = append(si.Stages, stageAndChildrenToStageInfo(&stage, append(parents, psc.Stage.Name))) 491 } 492 493 for _, s := range psc.Parallel { 494 stage := s 495 si.Parallel = append(si.Parallel, stageAndChildrenToStageInfo(&stage, append(parents, psc.Stage.Name))) 496 } 497 498 return si 499 } 500 501 // PipelineRunMatches returns true if the pipeline run info matches the filter 502 func (o *PipelineRunInfoFilter) PipelineRunMatches(info *PipelineRunInfo) bool { 503 if o.Owner != "" && o.Owner != info.Organisation { 504 return false 505 } 506 if o.Repository != "" && o.Repository != info.Repository { 507 return false 508 } 509 if o.Branch != "" && strings.ToLower(o.Branch) != strings.ToLower(info.Branch) { 510 return false 511 } 512 if o.Build != "" && o.Build != info.Build { 513 return false 514 } 515 if o.Context != "" && o.Context != info.Context { 516 return false 517 } 518 if o.Filter != "" && !strings.Contains(info.Name, o.Filter) { 519 return false 520 } 521 if o.Pending { 522 status := info.Status() 523 if status != "Pending" && status != "Running" { 524 return false 525 } 526 } 527 return true 528 } 529 530 // BuildNumber returns the integer build number filter if specified 531 func (o *PipelineRunInfoFilter) BuildNumber() int { 532 text := o.Build 533 if text != "" { 534 answer, err := strconv.Atoi(text) 535 if err != nil { 536 return answer 537 } 538 } 539 return 0 540 } 541 542 // MatchesPipeline returns true if this build info matches the given pipeline 543 func (pri *PipelineRunInfo) MatchesPipeline(activity *v1.PipelineActivity) bool { 544 d := kube.CreatePipelineDetails(activity) 545 if d == nil { 546 return false 547 } 548 return d.GitOwner == pri.Organisation && d.GitRepository == pri.Repository && d.Build == pri.Build && strings.ToLower(d.BranchName) == strings.ToLower(pri.Branch) && d.Context == pri.Context 549 } 550 551 // Status returns the build status 552 func (pri *PipelineRunInfo) Status() string { 553 pod := pri.FindFirstStagePod() 554 if pod == nil { 555 return "No Pod" 556 } 557 return string(pod.Status.Phase) 558 } 559 560 // ToBuildPodInfo converts the object into a BuildPodInfo so it can be easily filtered 561 func (pri PipelineRunInfo) ToBuildPodInfo() *builds.BuildPodInfo { 562 answer := &builds.BuildPodInfo{ 563 Name: pri.Name, 564 Organisation: pri.Organisation, 565 Repository: pri.Repository, 566 Branch: pri.Branch, 567 Build: pri.Build, 568 BuildNumber: pri.BuildNumber, 569 Context: pri.Context, 570 Pipeline: pri.Pipeline, 571 LastCommitSHA: pri.LastCommitSHA, 572 LastCommitURL: pri.LastCommitURL, 573 LastCommitMessage: pri.LastCommitMessage, 574 GitInfo: pri.GitInfo, 575 } 576 pod := pri.FindFirstStagePod() 577 if pod != nil { 578 answer.Pod = pod 579 answer.PodName = pod.Name 580 containers := pod.Spec.Containers 581 if len(containers) > 0 { 582 answer.FirstStepImage = containers[0].Image 583 } 584 answer.CreatedTime = pod.CreationTimestamp.Time 585 } 586 return answer 587 } 588 589 // PipelineRunInfoOrder allows sorting of a slice of PipelineRunInfos 590 type PipelineRunInfoOrder []*PipelineRunInfo 591 592 func (a PipelineRunInfoOrder) Len() int { return len(a) } 593 func (a PipelineRunInfoOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 594 func (a PipelineRunInfoOrder) Less(i, j int) bool { 595 b1 := a[i] 596 b2 := a[j] 597 if b1.Organisation != b2.Organisation { 598 return b1.Organisation < b2.Organisation 599 } 600 if b1.Repository != b2.Repository { 601 return b1.Repository < b2.Repository 602 } 603 if b1.Branch != b2.Branch { 604 return b1.Branch < b2.Branch 605 } 606 return b1.BuildNumber > b2.BuildNumber 607 } 608 609 // SortPipelineRunInfos sorts a slice of PipelineRunInfos by their org, repo, branch, and build number 610 func SortPipelineRunInfos(pris []*PipelineRunInfo) { 611 sort.Sort(PipelineRunInfoOrder(pris)) 612 }