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

     1  /*
     2  Copyright 2017 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 main
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"net/http"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/sirupsen/logrus"
    28  
    29  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"sigs.k8s.io/prow/pkg/config"
    32  	"sigs.k8s.io/prow/pkg/tide"
    33  	"sigs.k8s.io/prow/pkg/tide/history"
    34  )
    35  
    36  type tidePools struct {
    37  	Queries     []string
    38  	TideQueries []config.TideQuery
    39  	Pools       []tide.PoolForDeck
    40  }
    41  
    42  type tideHistory struct {
    43  	History map[string][]history.Record
    44  }
    45  
    46  type tideAgent struct {
    47  	log          *logrus.Entry
    48  	path         string
    49  	updatePeriod func() time.Duration
    50  
    51  	// Config for hiding repos
    52  	hiddenRepos func() []string
    53  	hiddenOnly  bool
    54  	showHidden  bool
    55  
    56  	tenantIDs sets.Set[string]
    57  	cfg       func() *config.Config
    58  
    59  	sync.Mutex
    60  	pools   []tide.Pool
    61  	history map[string][]history.Record
    62  }
    63  
    64  func (ta *tideAgent) start() {
    65  	startTimePool := time.Now()
    66  	if err := ta.updatePools(); err != nil {
    67  		ta.log.WithError(err).Error("Updating pools the first time.")
    68  	}
    69  	startTimeHistory := time.Now()
    70  	if err := ta.updateHistory(); err != nil {
    71  		ta.log.WithError(err).Error("Updating history the first time.")
    72  	}
    73  
    74  	go func() {
    75  		for {
    76  			time.Sleep(time.Until(startTimePool.Add(ta.updatePeriod())))
    77  			startTimePool = time.Now()
    78  			if err := ta.updatePools(); err != nil {
    79  				ta.log.WithError(err).Error("Updating pools.")
    80  			}
    81  		}
    82  	}()
    83  	go func() {
    84  		for {
    85  			time.Sleep(time.Until(startTimeHistory.Add(ta.updatePeriod())))
    86  			startTimeHistory = time.Now()
    87  			if err := ta.updateHistory(); err != nil {
    88  				ta.log.WithError(err).Error("Updating history.")
    89  			}
    90  		}
    91  	}()
    92  }
    93  
    94  func fetchTideData(log *logrus.Entry, path string, data interface{}) error {
    95  	var prevErrs []error
    96  	var err error
    97  	backoff := 5 * time.Second
    98  	for i := 0; i < 4; i++ {
    99  		var resp *http.Response
   100  		if err != nil {
   101  			prevErrs = append(prevErrs, err)
   102  			time.Sleep(backoff)
   103  			backoff *= 4
   104  		}
   105  		resp, err = http.Get(path)
   106  		if err == nil {
   107  			defer resp.Body.Close()
   108  			if resp.StatusCode < 200 || resp.StatusCode > 299 {
   109  				err = fmt.Errorf("response has status code %d", resp.StatusCode)
   110  				continue
   111  			}
   112  			if err = json.NewDecoder(resp.Body).Decode(data); err != nil {
   113  				break
   114  			}
   115  			break
   116  		}
   117  	}
   118  
   119  	// Either combine previous errors with the returned error, or if we succeeded
   120  	// log once about any errors we saw before succeeding.
   121  	prevErr := utilerrors.NewAggregate(prevErrs)
   122  	if err != nil {
   123  		return utilerrors.NewAggregate([]error{err, prevErr})
   124  	}
   125  	if prevErr != nil {
   126  		log.WithError(prevErr).Infof(
   127  			"Failed %d retries fetching Tide data before success: %v.",
   128  			len(prevErrs),
   129  			prevErr,
   130  		)
   131  	}
   132  	return nil
   133  }
   134  
   135  func (ta *tideAgent) updatePools() error {
   136  	var pools []tide.Pool
   137  	if err := fetchTideData(ta.log, ta.path, &pools); err != nil {
   138  		return err
   139  	}
   140  	pools = ta.filterPools(pools)
   141  
   142  	ta.Lock()
   143  	defer ta.Unlock()
   144  	ta.pools = pools
   145  	return nil
   146  }
   147  
   148  func (ta *tideAgent) updateHistory() error {
   149  	path := strings.TrimSuffix(ta.path, "/") + "/history"
   150  	var history map[string][]history.Record
   151  	if err := fetchTideData(ta.log, path, &history); err != nil {
   152  		return err
   153  	}
   154  	history = ta.filterHistory(history)
   155  
   156  	ta.Lock()
   157  	defer ta.Unlock()
   158  	ta.history = history
   159  	return nil
   160  }
   161  
   162  func (ta *tideAgent) matchingIDs(ids []string) bool {
   163  	return len(ids) > 0 && ta.tenantIDs.HasAll(ids...)
   164  }
   165  
   166  func (ta *tideAgent) filterPools(pools []tide.Pool) []tide.Pool {
   167  	filtered := make([]tide.Pool, 0, len(pools))
   168  	for _, pool := range pools {
   169  		// curIDs are the IDs associated with all PJs in the Pool
   170  		// We want to add the ID associated with the OrgRepo for extra protection
   171  		curIDs := sets.New[string](pool.TenantIDs...)
   172  		orgRepoID := ta.cfg().GetProwJobDefault(pool.Org+"/"+pool.Repo, "*").TenantID
   173  		needsHide := matches(pool.Org+"/"+pool.Repo, ta.hiddenRepos())
   174  		if match := ta.filter(orgRepoID, curIDs, needsHide); match {
   175  			filtered = append(filtered, pool)
   176  		}
   177  	}
   178  	return filtered
   179  }
   180  
   181  func noTenantIDOrDefaultTenantID(ids []string) bool {
   182  	for _, id := range ids {
   183  		if id != "" && id != config.DefaultTenantID {
   184  			return false
   185  		}
   186  	}
   187  	return true
   188  }
   189  
   190  func recordIDs(records []history.Record) sets.Set[string] {
   191  	res := sets.Set[string]{}
   192  	for _, record := range records {
   193  		res.Insert(record.TenantIDs...)
   194  	}
   195  	return res
   196  }
   197  
   198  func (ta *tideAgent) filterHistory(hist map[string][]history.Record) map[string][]history.Record {
   199  	filtered := make(map[string][]history.Record, len(hist))
   200  	for pool, records := range hist {
   201  		orgRepo := strings.Split(pool, ":")[0]
   202  		curIDs := recordIDs(records).Insert()
   203  		orgRepoID := ta.cfg().GetProwJobDefault(orgRepo, "*").TenantID
   204  		needsHide := matches(orgRepo, ta.hiddenRepos())
   205  		if match := ta.filter(orgRepoID, curIDs, needsHide); match {
   206  			filtered[pool] = records
   207  		}
   208  	}
   209  	return filtered
   210  }
   211  
   212  func (ta *tideAgent) filter(orgRepoID string, curIDs sets.Set[string], needsHide bool) bool {
   213  	// If the orgrepo is associated with no tenantID OR the default tenantID we ignore it here.
   214  	// This prevents already IDd History from getting the default ID assigned to them when their orgrepo is not associated with an OrgRepo.
   215  	// History with no tenantID and with default tenantID behave the same, so adding the default ID just causes issues
   216  	if orgRepoID != "" && orgRepoID != config.DefaultTenantID {
   217  		curIDs.Insert(orgRepoID)
   218  	}
   219  	if len(ta.tenantIDs) > 0 {
   220  		if ta.matchingIDs(sets.List(curIDs)) {
   221  			// Deck has tenantIDs and they match with the History
   222  			return true
   223  		}
   224  	} else if needsHide {
   225  		if ta.showHidden || ta.hiddenOnly {
   226  			return true
   227  		}
   228  	} else if !ta.hiddenOnly && noTenantIDOrDefaultTenantID(sets.List(curIDs)) {
   229  		return true
   230  	}
   231  	return false
   232  
   233  }
   234  
   235  func (ta *tideAgent) filterQueries(queries []config.TideQuery) []config.TideQuery {
   236  	filtered := make([]config.TideQuery, 0, len(queries))
   237  	for _, qc := range queries {
   238  		curIDs := qc.TenantIDs(*ta.cfg())
   239  		var exposedRepos []string
   240  		for _, repo := range qc.Repos {
   241  			if !matches(repo, ta.hiddenRepos()) {
   242  				exposedRepos = append(exposedRepos, repo)
   243  			}
   244  		}
   245  		orgRepoID := ""
   246  		if len(exposedRepos) < len(qc.Repos) { // If there are hidden repos
   247  			if match := ta.filter(orgRepoID, sets.New[string](curIDs...), true); match {
   248  				filtered = append(filtered, qc)
   249  				continue
   250  			} else { // If the hidden repos result in this query being filtered out, then we should remove them and try the rest
   251  				qc.Repos = exposedRepos
   252  			}
   253  		}
   254  		if len(qc.Repos) > 0 || len(qc.Orgs) > 0 {
   255  			if match := ta.filter(orgRepoID, sets.New[string](curIDs...), false); match {
   256  				filtered = append(filtered, qc)
   257  			}
   258  		}
   259  	}
   260  	return filtered
   261  }
   262  
   263  // matches returns whether the provided repo intersects
   264  // with repos. repo has always the "org/repo" format but
   265  // repos can include both orgs and repos.
   266  func matches(repo string, repos []string) bool {
   267  	org := strings.Split(repo, "/")[0]
   268  	for _, r := range repos {
   269  		if r == repo || r == org {
   270  			return true
   271  		}
   272  	}
   273  	return false
   274  }