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 }