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 }