github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/tekton/pipelines.go (about) 1 package tekton 2 3 import ( 4 "fmt" 5 "reflect" 6 "sort" 7 "strconv" 8 "time" 9 10 jenkinsio "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io" 11 v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1" 12 clientv1 "github.com/jenkins-x/jx-api/pkg/client/clientset/versioned/typed/jenkins.io/v1" 13 "github.com/olli-ai/jx/v2/pkg/auth" 14 "github.com/olli-ai/jx/v2/pkg/cmd/clients" 15 "github.com/olli-ai/jx/v2/pkg/jxfactory" 16 "github.com/olli-ai/jx/v2/pkg/kube/naming" 17 "github.com/tektoncd/pipeline/pkg/apis/pipeline" 18 "k8s.io/apimachinery/pkg/labels" 19 "k8s.io/apimachinery/pkg/util/rand" 20 "k8s.io/client-go/kubernetes" 21 22 "github.com/jenkins-x/jx-api/pkg/client/clientset/versioned" 23 jxClient "github.com/jenkins-x/jx-api/pkg/client/clientset/versioned" 24 "github.com/jenkins-x/jx-logging/pkg/log" 25 "github.com/olli-ai/jx/v2/pkg/gits" 26 "github.com/olli-ai/jx/v2/pkg/kube" 27 "github.com/olli-ai/jx/v2/pkg/tekton/syntax" 28 "github.com/olli-ai/jx/v2/pkg/util" 29 "github.com/pkg/errors" 30 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" 31 pipelineapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" 32 tektonclient "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" 33 corev1 "k8s.io/api/core/v1" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 ) 36 37 // PipelineType is used to differentiate between actual build pipelines and pipelines to create the build pipelines, 38 // aka meta pipelines. 39 type PipelineType int 40 41 const ( 42 // BuildPipeline is the yype for the actual build pipeline 43 BuildPipeline PipelineType = iota 44 45 // MetaPipeline type for the meta pipeline used to generate the build pipeline 46 MetaPipeline 47 ) 48 49 func (s PipelineType) String() string { 50 return [...]string{"build", "meta"}[s] 51 } 52 53 // GeneratePipelineActivity generates a initial PipelineActivity CRD so UI/get act can get an earlier notification that the jobs have been scheduled 54 func GeneratePipelineActivity(buildNumber string, branch string, gitInfo *gits.GitRepository, context string, pr *PullRefs) *kube.PromoteStepActivityKey { 55 name := gitInfo.Organisation + "-" + gitInfo.Name + "-" + branch + "-" + buildNumber 56 57 pipeline := gitInfo.Organisation + "/" + gitInfo.Name + "/" + branch 58 log.Logger().Infof("PipelineActivity for %s", name) 59 key := &kube.PromoteStepActivityKey{ 60 PipelineActivityKey: kube.PipelineActivityKey{ 61 Name: name, 62 Pipeline: pipeline, 63 Build: buildNumber, 64 GitInfo: gitInfo, 65 Context: context, 66 }, 67 } 68 69 if pr != nil { 70 key.PullRefs = pr.ToMerge 71 } 72 73 return key 74 } 75 76 // CreateOrUpdateSourceResource lazily creates a Tekton Pipeline PipelineResource for the given git repository 77 func CreateOrUpdateSourceResource(tektonClient tektonclient.Interface, ns string, created *v1alpha1.PipelineResource) (*v1alpha1.PipelineResource, error) { 78 resourceName := created.Name 79 factory := jxfactory.NewFactory() 80 81 resourceClient, _, err := factory.CreateTektonPipelineResourceClient() 82 if err != nil { 83 return nil, errors.Wrap(err, "unable to create Tekton PipelineResource client") 84 } 85 resourceInterface := resourceClient.TektonV1alpha1().PipelineResources(ns) 86 87 _, err = resourceInterface.Create(created) 88 if err == nil { 89 return created, nil 90 } 91 92 answer, err2 := resourceInterface.Get(resourceName, metav1.GetOptions{}) 93 if err2 != nil { 94 return answer, errors.Wrapf(err, "failed to get PipelineResource %s with %v after failing to create a new one", resourceName, err2) 95 } 96 if !reflect.DeepEqual(&created.Spec, &answer.Spec) { 97 answer.Spec = created.Spec 98 answer, err = resourceInterface.Update(answer) 99 if err != nil { 100 return nil, errors.Wrapf(err, "failed to update PipelineResource %s", resourceName) 101 } 102 } 103 return answer, nil 104 } 105 106 // CreateOrUpdateTask lazily creates a Tekton Pipeline Task 107 func CreateOrUpdateTask(tektonClient tektonclient.Interface, ns string, created *v1alpha1.Task) (*v1alpha1.Task, error) { 108 resourceName := created.Name 109 if resourceName == "" { 110 return nil, fmt.Errorf("the Task must have a name") 111 } 112 resourceInterface := tektonClient.TektonV1alpha1().Tasks(ns) 113 114 _, err := resourceInterface.Create(created) 115 if err == nil { 116 return created, nil 117 } 118 119 answer, err2 := resourceInterface.Get(resourceName, metav1.GetOptions{}) 120 if err2 != nil { 121 return answer, errors.Wrapf(err, "failed to get PipelineResource %s with %v after failing to create a new one", resourceName, err2.Error()) 122 } 123 if !reflect.DeepEqual(&created.Spec, &answer.Spec) || !reflect.DeepEqual(created.Annotations, answer.Annotations) || !reflect.DeepEqual(created.Labels, answer.Labels) { 124 answer.Spec = created.Spec 125 answer.Labels = util.MergeMaps(answer.Labels, created.Labels) 126 answer.Annotations = util.MergeMaps(answer.Annotations, created.Annotations) 127 answer, err = resourceInterface.Update(answer) 128 if err != nil { 129 return nil, errors.Wrapf(err, "failed to update PipelineResource %s", resourceName) 130 } 131 } 132 return answer, nil 133 } 134 135 func nextBuildNumberFromActivity(activityInterface clientv1.PipelineActivityInterface, gitInfo *gits.GitRepository, branch string) (string, error) { 136 labelMap := labels.Set{ 137 "owner": gitInfo.Organisation, 138 "repository": gitInfo.Name, 139 "branch": branch, 140 } 141 142 activityList, err := kube.ListSelectedPipelineActivities(activityInterface, labelMap.AsSelector(), nil) 143 if err != nil { 144 return "", errors.Wrapf(err, "Unable to list pipeline activities for %s/%s/%s", gitInfo.Organisation, gitInfo.Name, branch) 145 } 146 if len(activityList.Items) == 0 { 147 return "1", nil 148 } 149 sort.Slice(activityList.Items, func(i, j int) bool { 150 iBuildNum, err := strconv.Atoi(activityList.Items[i].Spec.Build) 151 if err != nil { 152 iBuildNum = 0 153 } 154 jBuildNum, err := strconv.Atoi(activityList.Items[j].Spec.Build) 155 if err != nil { 156 jBuildNum = 0 157 } 158 return iBuildNum >= jBuildNum 159 }) 160 // Iterate over the sorted (highest to lowest build number) list of activities, returning a new build number 161 // as soon as we reach one we can parse to an int and add one to. 162 for _, activity := range activityList.Items { 163 actBuildNum, err := strconv.Atoi(activity.Spec.Build) 164 if err != nil { 165 continue 166 } 167 return strconv.Itoa(actBuildNum + 1), nil 168 } 169 // If we couldn't parse any build numbers, just set the next build number to 1. 170 return "1", nil 171 } 172 173 func nextBuildNumberFromSourceRepo(tektonClient tektonclient.Interface, jxClient jxClient.Interface, ns string, gitInfo *gits.GitRepository, branch string, context string) (string, error) { 174 resourceInterface := jxClient.JenkinsV1().SourceRepositories(ns) 175 // TODO: How does SourceRepository handle name overlap? 176 sourceRepoName := naming.ToValidName(gitInfo.Organisation + "-" + gitInfo.Name) 177 178 lastBuildNumber := 0 179 sourceRepo, err := kube.GetOrCreateSourceRepository(jxClient, ns, gitInfo.Name, gitInfo.Organisation, gitInfo.ProviderURL()) 180 if err != nil { 181 return "", errors.Wrapf(err, "Unable to generate next build number for %s/%s", sourceRepoName, branch) 182 } 183 sourceRepoName = sourceRepo.Name 184 if sourceRepo.Annotations == nil { 185 sourceRepo.Annotations = make(map[string]string, 1) 186 } 187 annKey := LastBuildNumberAnnotationPrefix + naming.ToValidName(branch) 188 annVal := sourceRepo.Annotations[annKey] 189 if annVal != "" { 190 lastBuildNumber, err = strconv.Atoi(annVal) 191 if err != nil { 192 return "", errors.Wrapf(err, "Expected number but SourceRepository %s has annotation %s with value %s\n", sourceRepoName, annKey, annVal) 193 } 194 } 195 196 for nextNumber := lastBuildNumber + 1; true; nextNumber++ { 197 // lets check there is not already a PipelineRun for this number 198 buildIdentifier := strconv.Itoa(nextNumber) 199 200 labelSelector := fmt.Sprintf("owner=%s,repo=%s,branch=%s,build=%s", gitInfo.Organisation, gitInfo.Name, branch, buildIdentifier) 201 if context != "" { 202 labelSelector += fmt.Sprintf(",context=%s", context) 203 } 204 205 prs, err := tektonClient.TektonV1alpha1().PipelineRuns(ns).List(metav1.ListOptions{ 206 LabelSelector: labelSelector, 207 }) 208 if err == nil && len(prs.Items) > 0 { 209 // lets try make another build number as there's already a PipelineRun 210 // which could be due to name clashes 211 continue 212 } 213 if sourceRepo != nil { 214 sourceRepo.Annotations[annKey] = buildIdentifier 215 if _, err := resourceInterface.Update(sourceRepo); err != nil { 216 return "", err 217 } 218 } 219 220 return buildIdentifier, nil 221 } 222 // We've somehow gotten here without determining the next build number, so let's error. 223 return "", fmt.Errorf("couldn't determine next build number for %s/%s/%s", gitInfo.Organisation, gitInfo.Name, branch) 224 } 225 226 // GenerateNextBuildNumber generates a new build number for the given project. 227 func GenerateNextBuildNumber(tektonClient tektonclient.Interface, jxClient jxClient.Interface, ns string, gitInfo *gits.GitRepository, branch string, duration time.Duration, context string, useActivity bool) (string, error) { 228 nextBuildNumber := "" 229 activityInterface := jxClient.JenkinsV1().PipelineActivities(ns) 230 231 f := func() error { 232 if useActivity { 233 bn, err := nextBuildNumberFromActivity(activityInterface, gitInfo, branch) 234 if err != nil { 235 return err 236 } 237 nextBuildNumber = bn 238 } else { 239 bn, err := nextBuildNumberFromSourceRepo(tektonClient, jxClient, ns, gitInfo, branch, context) 240 if err != nil { 241 return err 242 } 243 nextBuildNumber = bn 244 } 245 return nil 246 } 247 248 err := util.Retry(duration, f) 249 if err != nil { 250 return "", err 251 } 252 return nextBuildNumber, nil 253 } 254 255 // GenerateSourceRepoResource generates the PipelineResource for the git repository we are operating on. 256 func GenerateSourceRepoResource(name string, gitInfo *gits.GitRepository, revision string) *pipelineapi.PipelineResource { 257 if gitInfo == nil || gitInfo.HttpsURL() == "" { 258 return nil 259 260 } 261 262 // lets use the URL property as this preserves any provider specific paths; e.g. `/scm` on bitbucket server 263 u := gitInfo.URL 264 if u == "" { 265 u = gitInfo.HttpsURL() 266 } 267 resource := &pipelineapi.PipelineResource{ 268 TypeMeta: metav1.TypeMeta{ 269 APIVersion: syntax.TektonAPIVersion, 270 Kind: "PipelineResource", 271 }, 272 ObjectMeta: metav1.ObjectMeta{ 273 Name: name, 274 }, 275 Spec: pipelineapi.PipelineResourceSpec{ 276 Type: pipelineapi.PipelineResourceTypeGit, 277 Params: []pipelineapi.ResourceParam{ 278 { 279 Name: "revision", 280 Value: revision, 281 }, 282 { 283 Name: "url", 284 Value: u, 285 }, 286 }, 287 }, 288 } 289 290 return resource 291 } 292 293 // CreatePipelineRun creates the PipelineRun struct. 294 func CreatePipelineRun(resources []*pipelineapi.PipelineResource, 295 name string, 296 apiVersion string, 297 labels map[string]string, 298 serviceAccount string, 299 pipelineParams []pipelineapi.Param, 300 timeout *metav1.Duration, 301 affinity *corev1.Affinity, 302 tolerations []corev1.Toleration) *pipelineapi.PipelineRun { 303 var resourceBindings []pipelineapi.PipelineResourceBinding 304 for _, resource := range resources { 305 resourceBindings = append(resourceBindings, pipelineapi.PipelineResourceBinding{ 306 Name: resource.Name, 307 ResourceRef: &pipelineapi.PipelineResourceRef{ 308 Name: resource.Name, 309 APIVersion: resource.APIVersion, 310 }, 311 }) 312 } 313 314 if serviceAccount == "" { 315 serviceAccount = DefaultPipelineSA 316 } 317 if timeout == nil { 318 timeout = &metav1.Duration{Duration: 240 * time.Hour} 319 } 320 321 pipelineRun := &pipelineapi.PipelineRun{ 322 TypeMeta: metav1.TypeMeta{ 323 APIVersion: syntax.TektonAPIVersion, 324 Kind: "PipelineRun", 325 }, 326 ObjectMeta: metav1.ObjectMeta{ 327 Name: name, 328 Labels: util.MergeMaps(labels), 329 }, 330 Spec: pipelineapi.PipelineRunSpec{ 331 ServiceAccountName: serviceAccount, 332 PipelineRef: &pipelineapi.PipelineRef{ 333 Name: name, 334 APIVersion: apiVersion, 335 }, 336 Resources: resourceBindings, 337 Params: pipelineParams, 338 // TODO: We shouldn't have to set a default timeout in the first place. See https://github.com/tektoncd/pipeline/issues/978 339 Timeout: timeout, 340 PodTemplate: &pipelineapi.PodTemplate{ 341 Affinity: affinity, 342 Tolerations: tolerations, 343 }, 344 }, 345 } 346 347 return pipelineRun 348 } 349 350 // ApplyPipelineRun lazily creates a Tekton PipelineRun. 351 func ApplyPipelineRun(tektonClient tektonclient.Interface, ns string, run *v1alpha1.PipelineRun) (*v1alpha1.PipelineRun, error) { 352 resourceName := run.Name 353 resourceInterface := tektonClient.TektonV1alpha1().PipelineRuns(ns) 354 355 answer, err := resourceInterface.Create(run) 356 if err != nil { 357 return nil, errors.Wrapf(err, "Failed to create PipelineRun %s", resourceName) 358 } 359 return answer, nil 360 } 361 362 // CreateOrUpdatePipeline lazily creates a Tekton Pipeline for the given git repository, branch and context 363 func CreateOrUpdatePipeline(tektonClient tektonclient.Interface, ns string, created *v1alpha1.Pipeline) (*v1alpha1.Pipeline, error) { 364 resourceName := created.Name 365 resourceInterface := tektonClient.TektonV1alpha1().Pipelines(ns) 366 367 answer, err := resourceInterface.Create(created) 368 if err == nil { 369 return answer, nil 370 } 371 372 answer, err = resourceInterface.Get(resourceName, metav1.GetOptions{}) 373 if err != nil { 374 return answer, errors.Wrapf(err, "failed to get Pipeline %s after failing to create a new one", resourceName) 375 } 376 377 if !reflect.DeepEqual(&created.Spec, &answer.Spec) || !reflect.DeepEqual(created.Labels, answer.Labels) { 378 answer.Annotations = util.MergeMaps(answer.Annotations, created.Annotations) 379 answer.Spec = created.Spec 380 answer, err = resourceInterface.Update(answer) 381 if err != nil { 382 return nil, errors.Wrapf(err, "failed to update Pipeline %s", resourceName) 383 } 384 } 385 return answer, nil 386 } 387 388 // PipelineResourceNameFromGitInfo returns the pipeline resource name for the given git repository, branch and context 389 func PipelineResourceNameFromGitInfo(gitInfo *gits.GitRepository, branch string, context string, pipelineType string) string { 390 return PipelineResourceName(gitInfo.Organisation, gitInfo.Name, branch, context, pipelineType) 391 } 392 393 // PipelineResourceName returns the pipeline resource name for the given git org, repo name, branch and context. It will always be unique. 394 func PipelineResourceName(organisation string, name string, branch string, context string, pipelineType string) string { 395 return possiblyUniquePipelineResourceName(organisation, name, branch, context, pipelineType, true) 396 } 397 398 // possiblyUniquePipelineResourceName returns the pipeline resource name for the given git org, repo name, branch and context, possibly forcing it to be unique 399 func possiblyUniquePipelineResourceName(organisation string, name string, branch string, context string, pipelineType string, forceUnique bool) string { 400 dirtyName := organisation + "-" + name + "-" + branch 401 if context != "" { 402 dirtyName += "-" + context 403 } 404 405 if pipelineType == MetaPipeline.String() { 406 dirtyName = pipelineType + "-" + dirtyName 407 } 408 resourceName := naming.ToValidNameTruncated(dirtyName, 31) 409 410 if forceUnique { 411 return resourceName + "-" + rand.String(5) 412 } 413 return resourceName 414 } 415 416 // ApplyPipeline applies the tasks and pipeline to the cluster 417 // and creates and applies a PipelineResource for their source repo and a pipelineRun 418 // to execute them. 419 func ApplyPipeline(jxClient versioned.Interface, kubeClient kubernetes.Interface, tektonClient tektonclient.Interface, crds *CRDWrapper, ns string, activityKey *kube.PromoteStepActivityKey) error { 420 info := util.ColorInfo 421 422 var activityOwnerReference *metav1.OwnerReference 423 424 if activityKey != nil { 425 activity, _, err := activityKey.GetOrCreate(jxClient, crds.Pipeline().Namespace) 426 if err != nil { 427 return err 428 } 429 430 activityOwnerReference = &metav1.OwnerReference{ 431 APIVersion: jenkinsio.GroupAndVersion, 432 Kind: "PipelineActivity", 433 Name: activity.Name, 434 UID: activity.UID, 435 } 436 } 437 438 gitKind := "" 439 if activityKey != nil { 440 clusterAuthConfigSvc, err := clients.NewFactory().CreateGitAuthConfigService(ns, "") 441 if err != nil { 442 return errors.Wrapf(err, "getting cluster-based auth configmap for checking kind of git url %s", activityKey.GitInfo.HostURL()) 443 } 444 var clusterAuthConfig *auth.AuthConfig 445 if clusterAuthConfigSvc != nil { 446 clusterAuthConfig = clusterAuthConfigSvc.Config() 447 } 448 // Don't bother checking for an error here - we'll just fall back to no kind specified. 449 gitKind, _ = kube.GetGitServiceKind(jxClient, kubeClient, ns, clusterAuthConfig, activityKey.GitInfo.HostURL()) 450 } 451 for _, resource := range crds.Resources() { 452 if activityOwnerReference != nil { 453 resource.OwnerReferences = []metav1.OwnerReference{*activityOwnerReference} 454 } 455 _, err := CreateOrUpdateSourceResource(tektonClient, ns, resource) 456 if err != nil { 457 return errors.Wrapf(err, "failed to create/update PipelineResource %s in namespace %s", resource.Name, ns) 458 } 459 if resource.Spec.Type == pipelineapi.PipelineResourceTypeGit { 460 gitURL := gits.HttpCloneURL(activityKey.GitInfo, gitKind) 461 log.Logger().Infof("upserted PipelineResource %s for the git repository %s", info(resource.Name), info(gitURL)) 462 } else { 463 log.Logger().Infof("upserted PipelineResource %s", info(resource.Name)) 464 } 465 } 466 467 for _, task := range crds.Tasks() { 468 if activityOwnerReference != nil { 469 task.OwnerReferences = []metav1.OwnerReference{*activityOwnerReference} 470 } 471 _, err := CreateOrUpdateTask(tektonClient, ns, task) 472 if err != nil { 473 return errors.Wrapf(err, "failed to create/update the task %s in namespace %s", task.Name, ns) 474 } 475 log.Logger().Infof("upserted Task %s", info(task.Name)) 476 } 477 478 if activityOwnerReference != nil { 479 crds.Pipeline().OwnerReferences = []metav1.OwnerReference{*activityOwnerReference} 480 } 481 482 pipeline, err := CreateOrUpdatePipeline(tektonClient, ns, crds.Pipeline()) 483 if err != nil { 484 return errors.Wrapf(err, "failed to create/update the pipeline in namespace %s", ns) 485 } 486 log.Logger().Infof("upserted Pipeline %s", info(pipeline.Name)) 487 488 pipelineOwnerReference := metav1.OwnerReference{ 489 APIVersion: syntax.TektonAPIVersion, 490 Kind: "pipeline", 491 Name: pipeline.Name, 492 UID: pipeline.UID, 493 } 494 495 crds.structure.OwnerReferences = []metav1.OwnerReference{pipelineOwnerReference} 496 497 _, err = ApplyPipelineRun(tektonClient, ns, crds.PipelineRun()) 498 if err != nil { 499 return errors.Wrapf(err, "failed to create the pipelineRun in namespace %s", ns) 500 } 501 log.Logger().Infof("created PipelineRun %s", info(crds.PipelineRun().Name)) 502 503 if crds.Structure() != nil { 504 crds.Structure().PipelineRunRef = &crds.PipelineRun().Name 505 506 structuresClient := jxClient.JenkinsV1().PipelineStructures(ns) 507 508 // Reset the structure name to be the run's name and set the PipelineRef and PipelineRunRef 509 if crds.Structure().PipelineRef == nil { 510 crds.Structure().PipelineRef = &pipeline.Name 511 } 512 crds.Structure().Name = crds.PipelineRun().Name 513 crds.Structure().PipelineRunRef = &crds.PipelineRun().Name 514 515 if _, structErr := structuresClient.Create(crds.Structure()); structErr != nil { 516 return errors.Wrapf(structErr, "failed to create the PipelineStructure in namespace %s", ns) 517 } 518 log.Logger().Infof("created PipelineStructure %s", info(crds.Structure().Name)) 519 } 520 521 return nil 522 } 523 524 // StructureForPipelineRun finds the PipelineStructure for the given PipelineRun, trying its name first and then its 525 // Pipeline name, returning an error if no PipelineStructure can be found. 526 func StructureForPipelineRun(jxClient versioned.Interface, ns string, run *pipelineapi.PipelineRun) (*v1.PipelineStructure, error) { 527 // Use the Pipeline name for this run. 528 pipelineName := run.Labels[pipeline.GroupName+pipeline.PipelineLabelKey] 529 // Fall back on the PipelineRef.Name if there isn't a label. 530 if pipelineName == "" { 531 pipelineName = run.Spec.PipelineRef.Name 532 } 533 // If we still have no name, error out. 534 if pipelineName == "" { 535 return nil, fmt.Errorf("couldn't find a Pipeline name for PipelineRun %s", run.Name) 536 } 537 structure, err := jxClient.JenkinsV1().PipelineStructures(ns).Get(pipelineName, metav1.GetOptions{}) 538 if err != nil { 539 return nil, errors.Wrapf(err, "getting PipelineStructure with Pipeline name %s for PipelineRun %s", pipelineName, run.Name) 540 } 541 return structure, nil 542 } 543 544 // PipelineRunIsNotPending returns true if the PipelineRun has completed or has running steps. 545 func PipelineRunIsNotPending(pr *pipelineapi.PipelineRun) bool { 546 if pr.Status.CompletionTime != nil { 547 return true 548 } 549 if len(pr.Status.TaskRuns) > 0 { 550 for _, v := range pr.Status.TaskRuns { 551 if v.Status != nil { 552 for _, stepState := range v.Status.Steps { 553 if stepState.Waiting == nil || stepState.Waiting.Reason == "PodInitializing" { 554 return true 555 } 556 } 557 } 558 } 559 } 560 return false 561 } 562 563 // PipelineRunIsComplete returns true if the PipelineRun has completed or has running steps. 564 func PipelineRunIsComplete(pr *pipelineapi.PipelineRun) bool { 565 if pr.Status.CompletionTime != nil { 566 return true 567 } 568 return false 569 } 570 571 // CancelPipelineRun cancels a Pipeline 572 func CancelPipelineRun(tektonClient tektonclient.Interface, ns string, pr *pipelineapi.PipelineRun) error { 573 pr.Spec.Status = pipelineapi.PipelineRunSpecStatusCancelled 574 _, err := tektonClient.TektonV1alpha1().PipelineRuns(ns).Update(pr) 575 if err != nil { 576 return errors.Wrapf(err, "failed to update PipelineRun %s in namespace %s to mark it as cancelled", pr.Name, ns) 577 } 578 return nil 579 }