sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/cmd/tide/main.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  	"context"
    21  	"errors"
    22  	"flag"
    23  	"net/http"
    24  	"os"
    25  	"strconv"
    26  	"time"
    27  
    28  	"github.com/sirupsen/logrus"
    29  	"k8s.io/apimachinery/pkg/util/sets"
    30  	"sigs.k8s.io/controller-runtime/pkg/manager"
    31  	"sigs.k8s.io/prow/pkg/config"
    32  	"sigs.k8s.io/prow/pkg/pjutil/pprof"
    33  
    34  	"sigs.k8s.io/prow/pkg/flagutil"
    35  	prowflagutil "sigs.k8s.io/prow/pkg/flagutil"
    36  	configflagutil "sigs.k8s.io/prow/pkg/flagutil/config"
    37  	"sigs.k8s.io/prow/pkg/interrupts"
    38  	"sigs.k8s.io/prow/pkg/logrusutil"
    39  	"sigs.k8s.io/prow/pkg/metrics"
    40  	"sigs.k8s.io/prow/pkg/tide"
    41  )
    42  
    43  const (
    44  	githubProviderName = "github"
    45  	gerritProviderName = "gerrit"
    46  )
    47  
    48  type options struct {
    49  	port int
    50  
    51  	config configflagutil.ConfigOptions
    52  
    53  	syncThrottle   int
    54  	statusThrottle int
    55  
    56  	dryRun                 bool
    57  	runOnce                bool
    58  	kubernetes             prowflagutil.KubernetesOptions
    59  	github                 prowflagutil.GitHubOptions
    60  	gerrit                 prowflagutil.GerritOptions
    61  	storage                prowflagutil.StorageClientOptions
    62  	instrumentationOptions prowflagutil.InstrumentationOptions
    63  	controllerManager      prowflagutil.ControllerManagerOptions
    64  
    65  	maxRecordsPerPool int
    66  	// historyURI where Tide should store its action history.
    67  	// Can be /local/path, gs://path/to/object or s3://path/to/object.
    68  	// GCS writes will use the bucket's default acl for new objects. Ensure both that
    69  	// a) the gcs credentials can write to this bucket
    70  	// b) the default acls do not expose any private info
    71  	historyURI string
    72  
    73  	// statusURI where Tide store status update state.
    74  	// Can be a /local/path, gs://path/to/object or s3://path/to/object.
    75  	// GCS writes will use the bucket's default acl for new objects. Ensure both that
    76  	// a) the gcs credentials can write to this bucket
    77  	// b) the default acls do not expose any private info
    78  	statusURI string
    79  
    80  	// providerName is
    81  	providerName string
    82  
    83  	// Gerrit-related options
    84  	cookiefilePath string
    85  }
    86  
    87  func (o *options) Validate() error {
    88  	for _, group := range []flagutil.OptionGroup{&o.kubernetes, &o.storage, &o.config, &o.controllerManager} {
    89  		if err := group.Validate(o.dryRun); err != nil {
    90  			return err
    91  		}
    92  	}
    93  	if o.providerName != "" && !sets.NewString(githubProviderName, gerritProviderName).Has(o.providerName) {
    94  		return errors.New("--provider should be github or gerrit")
    95  	}
    96  	var providerFlagGroup flagutil.OptionGroup = &o.github
    97  	if o.providerName == gerritProviderName {
    98  		providerFlagGroup = &o.gerrit
    99  	}
   100  	if err := providerFlagGroup.Validate(o.dryRun); err != nil {
   101  		return err
   102  	}
   103  	return nil
   104  }
   105  
   106  func gatherOptions(fs *flag.FlagSet, args ...string) options {
   107  	var o options
   108  	fs.IntVar(&o.port, "port", 8888, "Port to listen on.")
   109  	fs.BoolVar(&o.dryRun, "dry-run", true, "Whether to mutate any real-world state.")
   110  	fs.BoolVar(&o.runOnce, "run-once", false, "If true, run only once then quit.")
   111  	o.github.AddCustomizedFlags(fs, prowflagutil.DisableThrottlerOptions())
   112  	for _, group := range []flagutil.OptionGroup{&o.kubernetes, &o.storage, &o.instrumentationOptions, &o.config, &o.gerrit} {
   113  		group.AddFlags(fs)
   114  	}
   115  	fs.IntVar(&o.syncThrottle, "sync-hourly-tokens", 800, "The maximum number of tokens per hour to be used by the sync controller.")
   116  	fs.IntVar(&o.statusThrottle, "status-hourly-tokens", 400, "The maximum number of tokens per hour to be used by the status controller.")
   117  	fs.IntVar(&o.maxRecordsPerPool, "max-records-per-pool", 1000, "The maximum number of history records stored for an individual Tide pool.")
   118  	fs.StringVar(&o.historyURI, "history-uri", "", "The /local/path,gs://path/to/object or s3://path/to/object to store tide action history. GCS writes will use the default object ACL for the bucket")
   119  	fs.StringVar(&o.statusURI, "status-path", "", "The /local/path, gs://path/to/object or s3://path/to/object to store status controller state. GCS writes will use the default object ACL for the bucket.")
   120  	// Gerrit-related flags
   121  	fs.StringVar(&o.cookiefilePath, "cookiefile", "", "Path to git http.cookiefile; leave empty for anonymous access or if you are using GitHub")
   122  
   123  	fs.StringVar(&o.providerName, "provider", "", "The source code provider, only supported providers are github and gerrit, this should be set only when both GitHub and Gerrit configs are set for tide. By default provider is auto-detected as github if `tide.queries` is set, and gerrit if `tide.gerrit` is set.")
   124  	o.controllerManager.TimeoutListingProwJobsDefault = 30 * time.Second
   125  	o.controllerManager.AddFlags(fs)
   126  	fs.Parse(args)
   127  	return o
   128  }
   129  
   130  func main() {
   131  	logrusutil.ComponentInit()
   132  
   133  	defer interrupts.WaitForGracefulShutdown()
   134  
   135  	o := gatherOptions(flag.NewFlagSet(os.Args[0], flag.ExitOnError), os.Args[1:]...)
   136  	if err := o.Validate(); err != nil {
   137  		logrus.WithError(err).Fatal("Invalid options")
   138  	}
   139  
   140  	pprof.Instrument(o.instrumentationOptions)
   141  
   142  	opener, err := o.storage.StorageClient(context.Background())
   143  	if err != nil {
   144  		logrus.WithError(err).Fatal("Cannot create opener")
   145  	}
   146  
   147  	configAgent, err := o.config.ConfigAgent()
   148  	if err != nil {
   149  		logrus.WithError(err).Fatal("Error starting config agent.")
   150  	}
   151  	cfg := configAgent.Config
   152  
   153  	kubeCfg, err := o.kubernetes.InfrastructureClusterConfig(o.dryRun)
   154  	if err != nil {
   155  		logrus.WithError(err).Fatal("Error getting kubeconfig.")
   156  	}
   157  	// Do not activate leader election here, as we do not use the `mgr` to control the lifecylcle of our cotrollers,
   158  	// this would just be a no-op.
   159  	mgr, err := manager.New(kubeCfg, manager.Options{Namespace: cfg().ProwJobNamespace, MetricsBindAddress: "0"})
   160  	if err != nil {
   161  		logrus.WithError(err).Fatal("Error constructing mgr.")
   162  	}
   163  
   164  	if cfg().Tide.Gerrit != nil && cfg().Tide.Queries.QueryMap() != nil && o.providerName == "" {
   165  		logrus.Fatal("Both github and gerrit are configured in tide config but provider is not set.")
   166  	}
   167  
   168  	var c *tide.Controller
   169  	gitClient, err := o.github.GitClientFactory(o.cookiefilePath, &o.config.InRepoConfigCacheDirBase, o.dryRun, false)
   170  	if err != nil {
   171  		logrus.WithError(err).Fatal("Error getting Git client.")
   172  	}
   173  	provider := provider(o.providerName, cfg().Tide)
   174  	switch provider {
   175  	case githubProviderName:
   176  		githubSync, err := o.github.GitHubClientWithLogFields(o.dryRun, logrus.Fields{"controller": "sync"})
   177  		if err != nil {
   178  			logrus.WithError(err).Fatal("Error getting GitHub client for sync.")
   179  		}
   180  
   181  		githubStatus, err := o.github.GitHubClientWithLogFields(o.dryRun, logrus.Fields{"controller": "status-update"})
   182  		if err != nil {
   183  			logrus.WithError(err).Fatal("Error getting GitHub client for status.")
   184  		}
   185  
   186  		// The sync loop should be allowed more tokens than the status loop because
   187  		// it has to list all PRs in the pool every loop while the status loop only
   188  		// has to list changed PRs every loop.
   189  		// The sync loop should have a much lower burst allowance than the status
   190  		// loop which may need to update many statuses upon restarting Tide after
   191  		// changing the context format or starting Tide on a new repo.
   192  		githubSync.Throttle(o.syncThrottle, 3*tokensPerIteration(o.syncThrottle, cfg().Tide.SyncPeriod.Duration))
   193  		githubStatus.Throttle(o.statusThrottle, o.statusThrottle/2)
   194  
   195  		c, err = tide.NewController(
   196  			githubSync,
   197  			githubStatus,
   198  			mgr,
   199  			cfg,
   200  			gitClient,
   201  			o.maxRecordsPerPool,
   202  			opener,
   203  			o.historyURI,
   204  			o.statusURI,
   205  			nil,
   206  			o.github.AppPrivateKeyPath != "",
   207  		)
   208  		if err != nil {
   209  			logrus.WithError(err).Fatal("Error creating Tide controller.")
   210  		}
   211  	case gerritProviderName:
   212  		c, err = tide.NewGerritController(
   213  			mgr,
   214  			configAgent,
   215  			gitClient,
   216  			o.maxRecordsPerPool,
   217  			opener,
   218  			o.historyURI,
   219  			o.statusURI,
   220  			nil,
   221  			o.config,
   222  			o.cookiefilePath,
   223  			o.gerrit.MaxQPS,
   224  			o.gerrit.MaxBurst,
   225  		)
   226  		if err != nil {
   227  			logrus.WithError(err).Fatal("Error creating Tide controller.")
   228  		}
   229  	default:
   230  		logrus.Fatalf("Unsupported provider type '%s', this should not happen", provider)
   231  	}
   232  
   233  	interrupts.Run(func(ctx context.Context) {
   234  		if err := mgr.Start(ctx); err != nil {
   235  			logrus.WithError(err).Fatal("Mgr failed.")
   236  		}
   237  		logrus.Info("Mgr finished gracefully.")
   238  	})
   239  
   240  	mgrSyncCtx, mgrSyncCtxCancel := context.WithTimeout(context.Background(), o.controllerManager.TimeoutListingProwJobs)
   241  	defer mgrSyncCtxCancel()
   242  	if synced := mgr.GetCache().WaitForCacheSync(mgrSyncCtx); !synced {
   243  		logrus.Fatal("Timed out waiting for cachesync")
   244  	}
   245  
   246  	interrupts.OnInterrupt(func() {
   247  		c.Shutdown()
   248  		if err := gitClient.Clean(); err != nil {
   249  			logrus.WithError(err).Error("Could not clean up git client cache.")
   250  		}
   251  	})
   252  
   253  	// Deck consumes these endpoints
   254  	controllerMux := http.NewServeMux()
   255  	controllerMux.Handle("/", c)
   256  	controllerMux.Handle("/history", c.History())
   257  	server := &http.Server{Addr: ":" + strconv.Itoa(o.port), Handler: controllerMux}
   258  
   259  	// Push metrics to the configured prometheus pushgateway endpoint or serve them
   260  	metrics.ExposeMetrics("tide", cfg().PushGateway, o.instrumentationOptions.MetricsPort)
   261  
   262  	start := time.Now()
   263  	sync(c)
   264  	if o.runOnce {
   265  		return
   266  	}
   267  
   268  	// serve data
   269  	interrupts.ListenAndServe(server, 10*time.Second)
   270  
   271  	// run the controller, but only after one sync period expires after our first run
   272  	time.Sleep(time.Until(start.Add(cfg().Tide.SyncPeriod.Duration)))
   273  	interrupts.Tick(func() {
   274  		sync(c)
   275  	}, func() time.Duration {
   276  		return cfg().Tide.SyncPeriod.Duration
   277  	})
   278  }
   279  
   280  func sync(c *tide.Controller) {
   281  	if err := c.Sync(); err != nil {
   282  		logrus.WithError(err).Error("Error syncing.")
   283  	}
   284  }
   285  
   286  func provider(wantProvider string, tideConfig config.Tide) string {
   287  	if wantProvider != "" {
   288  		if !sets.NewString(githubProviderName, gerritProviderName).Has(wantProvider) {
   289  			return ""
   290  		}
   291  		return wantProvider
   292  	}
   293  	// Default to GitHub if GitHub queries are configured
   294  	if len([]config.TideQuery(tideConfig.Queries)) > 0 {
   295  		return githubProviderName
   296  	}
   297  	if tideConfig.Gerrit != nil && len([]config.GerritOrgRepoConfig(tideConfig.Gerrit.Queries)) > 0 {
   298  		return gerritProviderName
   299  	}
   300  	// When nothing is configured, don't fail tide. Assuming
   301  	return githubProviderName
   302  }
   303  
   304  func tokensPerIteration(hourlyTokens int, iterPeriod time.Duration) int {
   305  	tokenRate := float64(hourlyTokens) / float64(time.Hour)
   306  	return int(tokenRate * float64(iterPeriod))
   307  }