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

     1  /*
     2  Copyright 2018 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 jobs implements methods on job information used by Prow component deck
    18  package jobs
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	stdio "io"
    26  	"net/http"
    27  	"sort"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/sirupsen/logrus"
    32  	"k8s.io/apimachinery/pkg/labels"
    33  	"k8s.io/apimachinery/pkg/util/sets"
    34  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    35  
    36  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    37  	"sigs.k8s.io/prow/pkg/config"
    38  )
    39  
    40  const (
    41  	period = 30 * time.Second
    42  )
    43  
    44  var (
    45  	errProwjobNotFound = errors.New("prowjob not found")
    46  )
    47  
    48  func IsErrProwJobNotFound(err error) bool {
    49  	return err == errProwjobNotFound
    50  }
    51  
    52  // Job holds information about a job prow is running/has run.
    53  // TODO(#5216): Remove this, and all associated machinery.
    54  type Job struct {
    55  	Type        string               `json:"type"`
    56  	Refs        prowapi.Refs         `json:"refs"`
    57  	RefsKey     string               `json:"refs_key"`
    58  	Job         string               `json:"job"`
    59  	BuildID     string               `json:"build_id"`
    60  	Context     string               `json:"context"`
    61  	Started     string               `json:"started"`
    62  	Finished    string               `json:"finished"`
    63  	Duration    string               `json:"duration"`
    64  	State       string               `json:"state"`
    65  	Description string               `json:"description"`
    66  	URL         string               `json:"url"`
    67  	PodName     string               `json:"pod_name"`
    68  	Agent       prowapi.ProwJobAgent `json:"agent"`
    69  	ProwJob     string               `json:"prow_job"`
    70  
    71  	st time.Time
    72  	ft time.Time
    73  }
    74  
    75  type serviceClusterClient interface {
    76  	ListProwJobs(selector string) ([]prowapi.ProwJob, error)
    77  }
    78  
    79  // PodLogClient is an interface for interacting with the pod logs.
    80  type PodLogClient interface {
    81  	GetLogs(name, container string) ([]byte, error)
    82  }
    83  
    84  // PJListingClient is an interface to list ProwJobs
    85  type PJListingClient interface {
    86  	List(context.Context, *prowapi.ProwJobList, ...ctrlruntimeclient.ListOption) error
    87  }
    88  
    89  // NewJobAgent is a JobAgent constructor.
    90  func NewJobAgent(ctx context.Context, pjLister PJListingClient, hiddenOnly, showHidden bool, tenantIDs []string, plClients map[string]PodLogClient, cfg config.Getter) *JobAgent {
    91  	return &JobAgent{
    92  		kc: &filteringProwJobLister{
    93  			ctx:         ctx,
    94  			client:      pjLister,
    95  			hiddenRepos: func() sets.Set[string] { return sets.New[string](cfg().Deck.HiddenRepos...) },
    96  			hiddenOnly:  hiddenOnly,
    97  			showHidden:  showHidden,
    98  			tenantIDs:   tenantIDs,
    99  			cfg:         cfg,
   100  		},
   101  		pkcs:   plClients,
   102  		config: cfg,
   103  	}
   104  }
   105  
   106  type filteringProwJobLister struct {
   107  	ctx         context.Context
   108  	client      PJListingClient
   109  	cfg         config.Getter
   110  	hiddenRepos func() sets.Set[string]
   111  	hiddenOnly  bool
   112  	showHidden  bool
   113  	tenantIDs   []string
   114  }
   115  
   116  func (c *filteringProwJobLister) TenantIDMatch(pj prowapi.ProwJob) bool {
   117  	if pj.Spec.ProwJobDefault == nil {
   118  		return false
   119  	}
   120  	for _, id := range c.tenantIDs {
   121  		if id == pj.Spec.ProwJobDefault.TenantID {
   122  			return true
   123  		}
   124  	}
   125  	return false
   126  }
   127  
   128  func tenantIDMissingOrDefault(pj prowapi.ProwJob) bool {
   129  	return pj.Spec.ProwJobDefault == nil || pj.Spec.ProwJobDefault.TenantID == "" || pj.Spec.ProwJobDefault.TenantID == config.DefaultTenantID
   130  }
   131  
   132  func (c *filteringProwJobLister) ListProwJobs(selector string) ([]prowapi.ProwJob, error) {
   133  	prowJobList := &prowapi.ProwJobList{}
   134  	parsedSelector, err := labels.Parse(selector)
   135  	if err != nil {
   136  		return nil, fmt.Errorf("failed to parse selector: %w", err)
   137  	}
   138  	listOpts := &ctrlruntimeclient.ListOptions{LabelSelector: parsedSelector, Namespace: c.cfg().ProwJobNamespace}
   139  	if err := c.client.List(c.ctx, prowJobList, listOpts); err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	var filtered []prowapi.ProwJob
   144  	for _, item := range prowJobList.Items {
   145  		if len(c.tenantIDs) != 0 {
   146  			if c.TenantIDMatch(item) {
   147  				// Deck has tenantID and it matches Prowjob
   148  				filtered = append(filtered, item)
   149  			}
   150  		} else if len(c.tenantIDs) == 0 {
   151  			// Deck has no tenantID
   152  			shouldHide := item.Spec.Hidden || c.pjHasHiddenRefs(item)
   153  			if shouldHide && (c.showHidden || c.hiddenOnly) {
   154  				// If Hidden and we are showing Hidden we add it
   155  				filtered = append(filtered, item)
   156  			} else if !shouldHide && !c.hiddenOnly && tenantIDMissingOrDefault(item) {
   157  				// If not Hidden then show if not hiddenOnly AND if no tenantID
   158  				filtered = append(filtered, item)
   159  			}
   160  		}
   161  	}
   162  
   163  	return filtered, nil
   164  }
   165  
   166  func (c *filteringProwJobLister) pjHasHiddenRefs(pj prowapi.ProwJob) bool {
   167  	allRefs := pj.Spec.ExtraRefs
   168  	if pj.Spec.Refs != nil {
   169  		allRefs = append(allRefs, *pj.Spec.Refs)
   170  	}
   171  	for _, refs := range allRefs {
   172  		if c.hiddenRepos().HasAny(fmt.Sprintf("%s/%s", refs.Org, refs.Repo), refs.Org) {
   173  			return true
   174  		}
   175  	}
   176  
   177  	return false
   178  }
   179  
   180  // JobAgent creates lists of jobs, updates their status and returns their run logs.
   181  type JobAgent struct {
   182  	kc        serviceClusterClient
   183  	pkcs      map[string]PodLogClient
   184  	config    config.Getter
   185  	prowJobs  []prowapi.ProwJob
   186  	jobs      []Job
   187  	jobsMap   map[string]Job                        // pod name -> Job
   188  	jobsIDMap map[string]map[string]prowapi.ProwJob // job name -> id -> ProwJob
   189  	mut       sync.Mutex
   190  }
   191  
   192  // Start will start the job and periodically update it.
   193  func (ja *JobAgent) Start() {
   194  	ja.tryUpdate()
   195  	go func() {
   196  		t := time.Tick(period)
   197  		for range t {
   198  			ja.tryUpdate()
   199  		}
   200  	}()
   201  }
   202  
   203  // Jobs returns a thread-safe snapshot of the current job state.
   204  func (ja *JobAgent) Jobs() []Job {
   205  	ja.mut.Lock()
   206  	defer ja.mut.Unlock()
   207  	res := make([]Job, len(ja.jobs))
   208  	copy(res, ja.jobs)
   209  	return res
   210  }
   211  
   212  // ProwJobs returns a thread-safe snapshot of the current prow jobs.
   213  func (ja *JobAgent) ProwJobs() []prowapi.ProwJob {
   214  	ja.mut.Lock()
   215  	defer ja.mut.Unlock()
   216  	res := make([]prowapi.ProwJob, len(ja.prowJobs))
   217  	copy(res, ja.prowJobs)
   218  	return res
   219  }
   220  
   221  // GetProwJob finds the corresponding Prowjob resource from the provided job name and build ID
   222  func (ja *JobAgent) GetProwJob(job, id string) (prowapi.ProwJob, error) {
   223  	if ja == nil {
   224  		return prowapi.ProwJob{}, fmt.Errorf("Prow job agent doesn't exist (are you running locally?)")
   225  	}
   226  	var j prowapi.ProwJob
   227  	ja.mut.Lock()
   228  	idMap, ok := ja.jobsIDMap[job]
   229  	if ok {
   230  		j, ok = idMap[id]
   231  	}
   232  	ja.mut.Unlock()
   233  	if !ok {
   234  		return prowapi.ProwJob{}, errProwjobNotFound
   235  	}
   236  	return j, nil
   237  }
   238  
   239  // GetJobLog returns the job logs, works for both kubernetes and jenkins agent types.
   240  func (ja *JobAgent) GetJobLog(job, id string, container string) ([]byte, error) {
   241  	j, err := ja.GetProwJob(job, id)
   242  	if err != nil {
   243  		return nil, fmt.Errorf("error getting prowjob: %w", err)
   244  	}
   245  	if j.Spec.Agent == prowapi.KubernetesAgent {
   246  		client, ok := ja.pkcs[j.ClusterAlias()]
   247  		if !ok {
   248  			return nil, fmt.Errorf("cannot get logs for prowjob %q with agent %q: unknown cluster alias %q", j.ObjectMeta.Name, j.Spec.Agent, j.ClusterAlias())
   249  		}
   250  		return client.GetLogs(j.Status.PodName, container)
   251  	}
   252  	for _, agentToTmpl := range ja.config().Deck.ExternalAgentLogs {
   253  		if agentToTmpl.Agent != string(j.Spec.Agent) {
   254  			continue
   255  		}
   256  		if !agentToTmpl.Selector.Matches(labels.Set(j.ObjectMeta.Labels)) {
   257  			continue
   258  		}
   259  		var b bytes.Buffer
   260  		if err := agentToTmpl.URLTemplate.Execute(&b, &j); err != nil {
   261  			return nil, fmt.Errorf("cannot execute URL template for prowjob %q with agent %q: %w", j.ObjectMeta.Name, j.Spec.Agent, err)
   262  		}
   263  		resp, err := http.Get(b.String())
   264  		if err != nil {
   265  			return nil, err
   266  		}
   267  		defer resp.Body.Close()
   268  		return stdio.ReadAll(resp.Body)
   269  
   270  	}
   271  	return nil, fmt.Errorf("cannot get logs for prowjob %q with agent %q: the agent is missing from the prow config file", j.ObjectMeta.Name, j.Spec.Agent)
   272  }
   273  
   274  func (ja *JobAgent) tryUpdate() {
   275  	if err := ja.update(); err != nil {
   276  		logrus.WithError(err).Warning("Error updating job list.")
   277  	}
   278  }
   279  
   280  type byPJStartTime []prowapi.ProwJob
   281  
   282  func (a byPJStartTime) Len() int      { return len(a) }
   283  func (a byPJStartTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   284  func (a byPJStartTime) Less(i, j int) bool {
   285  	if a[i].Status.StartTime.Time != a[j].Status.StartTime.Time {
   286  		return a[i].Status.StartTime.Time.After(a[j].Status.StartTime.Time)
   287  	}
   288  	// Start time only has second granularity and we often start many jobs in the
   289  	// same second. Use the name as tie breaker.
   290  	return a[i].Spec.Job < a[j].Spec.Job
   291  }
   292  
   293  func (ja *JobAgent) update() error {
   294  	pjs, err := ja.kc.ListProwJobs(labels.Everything().String())
   295  	if err != nil {
   296  		return err
   297  	}
   298  	var njs []Job
   299  	njsMap := make(map[string]Job)
   300  	njsIDMap := make(map[string]map[string]prowapi.ProwJob)
   301  
   302  	sort.Sort(byPJStartTime(pjs))
   303  
   304  	for _, j := range pjs {
   305  		ft := time.Time{}
   306  		if j.Status.CompletionTime != nil {
   307  			ft = j.Status.CompletionTime.Time
   308  		}
   309  		buildID := j.Status.BuildID
   310  		nj := Job{
   311  			Type:    string(j.Spec.Type),
   312  			Job:     j.Spec.Job,
   313  			Context: j.Spec.Context,
   314  			Agent:   j.Spec.Agent,
   315  			ProwJob: j.ObjectMeta.Name,
   316  			BuildID: buildID,
   317  
   318  			Started:     fmt.Sprintf("%d", j.Status.StartTime.Time.Unix()),
   319  			State:       string(j.Status.State),
   320  			Description: j.Status.Description,
   321  			PodName:     j.Status.PodName,
   322  			URL:         j.Status.URL,
   323  
   324  			st: j.Status.StartTime.Time,
   325  			ft: ft,
   326  		}
   327  		if !nj.ft.IsZero() {
   328  			nj.Finished = nj.ft.Format(time.RFC3339Nano)
   329  			duration := nj.ft.Sub(nj.st)
   330  			duration -= duration % time.Second // strip fractional seconds
   331  			nj.Duration = duration.String()
   332  		}
   333  		if j.Spec.Refs != nil {
   334  			nj.Refs = *j.Spec.Refs
   335  			nj.RefsKey = j.Spec.Refs.String()
   336  		}
   337  		njs = append(njs, nj)
   338  		if nj.PodName != "" {
   339  			njsMap[nj.PodName] = nj
   340  		}
   341  		if _, ok := njsIDMap[j.Spec.Job]; !ok {
   342  			njsIDMap[j.Spec.Job] = make(map[string]prowapi.ProwJob)
   343  		}
   344  		njsIDMap[j.Spec.Job][buildID] = j
   345  	}
   346  
   347  	ja.mut.Lock()
   348  	defer ja.mut.Unlock()
   349  	ja.prowJobs = pjs
   350  	ja.jobs = njs
   351  	ja.jobsMap = njsMap
   352  	ja.jobsIDMap = njsIDMap
   353  	return nil
   354  }