github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/pjutil/abort.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package pjutil
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"sort"
    24  	"strings"
    25  
    26  	jsonpatch "github.com/evanphx/json-patch"
    27  	"github.com/sirupsen/logrus"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	ktypes "k8s.io/apimachinery/pkg/types"
    30  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    31  
    32  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    33  	reporter "sigs.k8s.io/prow/pkg/crier/reporters/github"
    34  )
    35  
    36  // patchClient a minimalistic prow client required by the aborter
    37  type patchClient interface {
    38  	Patch(ctx context.Context, obj ctrlruntimeclient.Object, patch ctrlruntimeclient.Patch, opts ...ctrlruntimeclient.PatchOption) error
    39  }
    40  
    41  // prowClient a minimalistic prow client required by the aborter
    42  type prowClient interface {
    43  	Patch(ctx context.Context, name string, pt ktypes.PatchType, data []byte, o metav1.PatchOptions, subresources ...string) (result *prowapi.ProwJob, err error)
    44  }
    45  
    46  // digestRefs digests a Refs to the fields we care about
    47  // for termination, ensuring that permutations of pulls
    48  // do not cause different digests
    49  func digestRefs(ref prowapi.Refs) string {
    50  	var pulls []int
    51  	for _, pull := range ref.Pulls {
    52  		pulls = append(pulls, pull.Number)
    53  	}
    54  	sort.Ints(pulls)
    55  	return fmt.Sprintf("%s/%s@%s %v", ref.Org, ref.Repo, ref.BaseRef, pulls)
    56  }
    57  
    58  // TerminateOlderJobs aborts all presubmit jobs from the given list that have a newer version. It does not set
    59  // the prowjob to complete. The responsible agent is expected to react to the aborted state by aborting the actual
    60  // test payload and then setting the ProwJob to completed.
    61  func TerminateOlderJobs(pjc patchClient, log *logrus.Entry, pjs []prowapi.ProwJob) error {
    62  	dupes := map[string]int{}
    63  	for i, pj := range pjs {
    64  		if pj.Complete() || pj.Spec.Type != prowapi.PresubmitJob {
    65  			continue
    66  		}
    67  
    68  		// we want to use salient fields of the job spec to create
    69  		// an identifier, so we digest the job spec and to ensure
    70  		// reentrancy, we must sort all of the slices in our identifier
    71  		// so that equivalent permutations of the refs map to the
    72  		// same identifier. We do not want commit hashes to matter
    73  		// here as a test for a newer set of commits but for the
    74  		// same set of names can abort older versions. We digest
    75  		// into strings as Go doesn't define equality for slices,
    76  		// so they are not valid to use in map keys.
    77  		identifiers := []string{
    78  			string(pj.Spec.Type),
    79  			pj.Spec.Job,
    80  		}
    81  		if pj.Spec.Refs != nil {
    82  			identifiers = append(identifiers, digestRefs(*pj.Spec.Refs))
    83  		}
    84  		for _, ref := range pj.Spec.ExtraRefs {
    85  			identifiers = append(identifiers, digestRefs(ref))
    86  		}
    87  
    88  		sort.Strings(identifiers)
    89  		ji := strings.Join(identifiers, ",")
    90  		prev, ok := dupes[ji]
    91  		if !ok {
    92  			dupes[ji] = i
    93  			continue
    94  		}
    95  		cancelIndex := i
    96  		if (&pjs[prev].Status.StartTime).Before(&pj.Status.StartTime) {
    97  			cancelIndex = prev
    98  			dupes[ji] = i
    99  		}
   100  		toCancel := pjs[cancelIndex]
   101  		prevPJ := toCancel.DeepCopy()
   102  
   103  		toCancel.Status.State = prowapi.AbortedState
   104  		if toCancel.Status.PrevReportStates == nil {
   105  			toCancel.Status.PrevReportStates = map[string]prowapi.ProwJobState{}
   106  		}
   107  		toCancel.Status.PrevReportStates[reporter.GitHubReporterName] = toCancel.Status.State
   108  
   109  		log.WithFields(ProwJobFields(&toCancel)).
   110  			WithField("from", prevPJ.Status.State).
   111  			WithField("to", toCancel.Status.State).Info("Transitioning states")
   112  
   113  		if err := pjc.Patch(context.Background(), &toCancel, ctrlruntimeclient.MergeFrom(prevPJ)); err != nil {
   114  			return err
   115  		}
   116  
   117  		// Update the cancelled jobs entry in pjs.
   118  		pjs[cancelIndex] = toCancel
   119  	}
   120  
   121  	return nil
   122  }
   123  
   124  func PatchProwjob(ctx context.Context, pjc prowClient, log *logrus.Entry, srcPJ prowapi.ProwJob, destPJ prowapi.ProwJob) (*prowapi.ProwJob, error) {
   125  	srcPJData, err := json.Marshal(srcPJ)
   126  	if err != nil {
   127  		return nil, fmt.Errorf("marshal source prow job: %w", err)
   128  	}
   129  
   130  	destPJData, err := json.Marshal(destPJ)
   131  	if err != nil {
   132  		return nil, fmt.Errorf("marshal dest prow job: %w", err)
   133  	}
   134  
   135  	patch, err := jsonpatch.CreateMergePatch(srcPJData, destPJData)
   136  	if err != nil {
   137  		return nil, fmt.Errorf("cannot create JSON patch: %w", err)
   138  	}
   139  
   140  	newPJ, err := pjc.Patch(ctx, srcPJ.Name, ktypes.MergePatchType, patch, metav1.PatchOptions{})
   141  	log.WithFields(ProwJobFields(&destPJ)).Debug("Patched ProwJob.")
   142  	return newPJ, err
   143  }