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  }