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 }