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