github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/tot/main.go (about) 1 /* 2 Copyright 2016 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 // Tot vends (rations) incrementing numbers for use in builds. 18 // https://en.wikipedia.org/wiki/Rum_ration 19 package main 20 21 import ( 22 "encoding/json" 23 "errors" 24 "flag" 25 "fmt" 26 "io" 27 "net/http" 28 "os" 29 "strconv" 30 "strings" 31 "sync" 32 "time" 33 34 "github.com/sirupsen/logrus" 35 "sigs.k8s.io/prow/pkg/pjutil/pprof" 36 37 "sigs.k8s.io/prow/pkg/config" 38 prowflagutil "sigs.k8s.io/prow/pkg/flagutil" 39 configflagutil "sigs.k8s.io/prow/pkg/flagutil/config" 40 "sigs.k8s.io/prow/pkg/interrupts" 41 "sigs.k8s.io/prow/pkg/logrusutil" 42 "sigs.k8s.io/prow/pkg/pjutil" 43 "sigs.k8s.io/prow/pkg/pod-utils/downwardapi" 44 "sigs.k8s.io/prow/pkg/pod-utils/gcs" 45 ) 46 47 type options struct { 48 port int 49 storagePath string 50 51 useFallback bool 52 fallbackURI string 53 54 config configflagutil.ConfigOptions 55 fallbackBucket string 56 instrumentationOptions prowflagutil.InstrumentationOptions 57 } 58 59 func gatherOptions() options { 60 o := options{} 61 flag.IntVar(&o.port, "port", 8888, "Port to listen on.") 62 flag.StringVar(&o.storagePath, "storage", "tot.json", "Where to store the results.") 63 64 flag.BoolVar(&o.useFallback, "fallback", false, "Fallback to GCS bucket for missing builds.") 65 flag.StringVar(&o.fallbackURI, "fallback-url-template", 66 "https://storage.googleapis.com/kubernetes-jenkins/logs/%s/latest-build.txt", 67 "URL template to fallback to for jobs that lack a last vended build number.", 68 ) 69 70 o.config.AddFlags(flag.CommandLine) 71 flag.StringVar(&o.fallbackBucket, "fallback-bucket", "", 72 "Fallback to top-level bucket for jobs that lack a last vended build number. The bucket layout is expected to follow https://github.com/kubernetes/test-infra/tree/master/gubernator#gcs-bucket-layout", 73 ) 74 o.instrumentationOptions.AddFlags(flag.CommandLine) 75 flag.Parse() 76 return o 77 } 78 79 func (o *options) Validate() error { 80 if err := o.config.Validate(false); err != nil { 81 return err 82 } 83 if o.config.ConfigPath == "" && o.fallbackBucket != "" { 84 return errors.New("you need to provide the prow config when a fallback bucket is specified") 85 } 86 return nil 87 } 88 89 type store struct { 90 Number map[string]int // job name -> last vended build number 91 mutex sync.Mutex 92 storagePath string 93 fallbackFunc func(string) int 94 } 95 96 func newStore(storagePath string) (*store, error) { 97 s := &store{ 98 Number: make(map[string]int), 99 storagePath: storagePath, 100 } 101 buf, err := os.ReadFile(storagePath) 102 if err == nil { 103 err = json.Unmarshal(buf, s) 104 if err != nil { 105 return nil, err 106 } 107 } else if !os.IsNotExist(err) { 108 return nil, err 109 } 110 return s, nil 111 } 112 113 func (s *store) save() error { 114 buf, err := json.Marshal(s) 115 if err != nil { 116 return err 117 } 118 err = os.WriteFile(s.storagePath+".tmp", buf, 0644) 119 if err != nil { 120 return err 121 } 122 return os.Rename(s.storagePath+".tmp", s.storagePath) 123 } 124 125 func (s *store) vend(jobName string) int { 126 s.mutex.Lock() 127 defer s.mutex.Unlock() 128 n, ok := s.Number[jobName] 129 if !ok && s.fallbackFunc != nil { 130 n = s.fallbackFunc(jobName) 131 } 132 n++ 133 134 s.Number[jobName] = n 135 136 err := s.save() 137 if err != nil { 138 logrus.Error(err) 139 } 140 141 return n 142 } 143 144 func (s *store) peek(jobName string) int { 145 s.mutex.Lock() 146 defer s.mutex.Unlock() 147 return s.Number[jobName] 148 } 149 150 func (s *store) set(jobName string, n int) { 151 s.mutex.Lock() 152 defer s.mutex.Unlock() 153 s.Number[jobName] = n 154 155 err := s.save() 156 if err != nil { 157 logrus.Error(err) 158 } 159 } 160 161 func (s *store) handle(w http.ResponseWriter, r *http.Request) { 162 jobName := r.URL.Path[len("/vend/"):] 163 switch r.Method { 164 case "GET": 165 n := s.vend(jobName) 166 logrus.Infof("Vending %s number %d to %s.", jobName, n, r.RemoteAddr) 167 fmt.Fprintf(w, "%d", n) 168 case "HEAD": 169 n := s.peek(jobName) 170 logrus.Infof("Peeking %s number %d to %s.", jobName, n, r.RemoteAddr) 171 fmt.Fprintf(w, "%d", n) 172 case "POST": 173 body, err := io.ReadAll(r.Body) 174 if err != nil { 175 logrus.WithError(err).Error("Unable to read body.") 176 return 177 } 178 n, err := strconv.Atoi(string(body)) 179 if err != nil { 180 logrus.WithError(err).Error("Unable to parse number.") 181 return 182 } 183 logrus.Infof("Setting %s to %d from %s.", jobName, n, r.RemoteAddr) 184 s.set(jobName, n) 185 } 186 } 187 188 type fallbackHandler struct { 189 template string 190 // in case a config agent is provided, tot will 191 // determine the GCS path that it needs to use 192 // based on the configured jobs in prow and 193 // bucket. 194 configAgent *config.Agent 195 bucket string 196 } 197 198 func (f fallbackHandler) get(jobName string) int { 199 url := f.getURL(jobName) 200 201 var body []byte 202 203 for i := 0; i < 10; i++ { 204 resp, err := http.Get(url) 205 if err == nil { 206 defer resp.Body.Close() 207 if resp.StatusCode == http.StatusOK { 208 body, err = io.ReadAll(resp.Body) 209 if err == nil { 210 break 211 } else { 212 logrus.WithError(err).Error("Failed to read response body.") 213 } 214 } else if resp.StatusCode == http.StatusNotFound { 215 break 216 } 217 } else { 218 logrus.WithError(err).Errorf("Failed to GET %s.", url) 219 } 220 time.Sleep(2 * time.Second) 221 } 222 223 n, err := strconv.Atoi(strings.TrimSpace(string(body))) 224 if err != nil { 225 return 0 226 } 227 228 return n 229 } 230 231 func (f fallbackHandler) getURL(jobName string) string { 232 if f.configAgent == nil { 233 return fmt.Sprintf(f.template, jobName) 234 } 235 236 var spec *downwardapi.JobSpec 237 cfg := f.configAgent.Config() 238 239 for _, pre := range cfg.AllStaticPresubmits(nil) { 240 if jobName == pre.Name { 241 spec = pjutil.PresubmitToJobSpec(pre) 242 break 243 } 244 } 245 if spec == nil { 246 for _, post := range cfg.AllStaticPostsubmits(nil) { 247 if jobName == post.Name { 248 spec = pjutil.PostsubmitToJobSpec(post) 249 break 250 } 251 } 252 } 253 if spec == nil { 254 for _, per := range cfg.AllPeriodics() { 255 if jobName == per.Name { 256 spec = pjutil.PeriodicToJobSpec(per) 257 break 258 } 259 } 260 } 261 // If spec is still nil, we know nothing about the requested job. 262 if spec == nil { 263 logrus.Errorf("requested job is unknown to prow: %s", jobName) 264 return "" 265 } 266 paths := gcs.LatestBuildForSpec(spec, nil) 267 if len(paths) != 1 { 268 logrus.Errorf("expected a single GCS path, got %v", paths) 269 return "" 270 } 271 return fmt.Sprintf("%s/%s", strings.TrimSuffix(f.bucket, "/"), paths[0]) 272 } 273 274 func main() { 275 logrusutil.ComponentInit() 276 277 o := gatherOptions() 278 if err := o.Validate(); err != nil { 279 logrus.Fatalf("Invalid options: %v", err) 280 } 281 282 defer interrupts.WaitForGracefulShutdown() 283 284 pprof.Instrument(o.instrumentationOptions) 285 health := pjutil.NewHealthOnPort(o.instrumentationOptions.HealthPort) 286 287 s, err := newStore(o.storagePath) 288 if err != nil { 289 logrus.WithError(err).Fatal("newStore failed") 290 } 291 292 if o.useFallback { 293 var configAgent *config.Agent 294 if o.config.ConfigPath != "" { 295 var err error 296 configAgent, err = o.config.ConfigAgent() 297 if err != nil { 298 logrus.WithError(err).Fatal("Error starting config agent.") 299 } 300 } 301 302 s.fallbackFunc = fallbackHandler{ 303 template: o.fallbackURI, 304 configAgent: configAgent, 305 bucket: o.fallbackBucket, 306 }.get 307 } 308 309 mux := http.NewServeMux() 310 mux.HandleFunc("/vend/", s.handle) 311 server := &http.Server{Addr: ":" + strconv.Itoa(o.port), Handler: mux} 312 health.ServeReady() 313 interrupts.ListenAndServe(server, 5*time.Second) 314 }