github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/gangway/main.go (about) 1 /* 2 Copyright 2022 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 "flag" 22 "fmt" 23 "net" 24 "os" 25 "os/exec" 26 "strconv" 27 "time" 28 29 grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 30 "github.com/sirupsen/logrus" 31 "google.golang.org/grpc" 32 "google.golang.org/grpc/health" 33 healthpb "google.golang.org/grpc/health/grpc_health_v1" 34 "google.golang.org/grpc/reflection" 35 utilerrors "k8s.io/apimachinery/pkg/util/errors" 36 37 "sigs.k8s.io/prow/pkg/config" 38 "sigs.k8s.io/prow/pkg/flagutil" 39 prowflagutil "sigs.k8s.io/prow/pkg/flagutil" 40 configflagutil "sigs.k8s.io/prow/pkg/flagutil/config" 41 "sigs.k8s.io/prow/pkg/gangway" 42 "sigs.k8s.io/prow/pkg/interrupts" 43 "sigs.k8s.io/prow/pkg/logrusutil" 44 "sigs.k8s.io/prow/pkg/metrics" 45 "sigs.k8s.io/prow/pkg/moonraker" 46 "sigs.k8s.io/prow/pkg/pjutil" 47 ) 48 49 // Empty string represents the overall health of all gRPC services. See 50 // https://github.com/grpc/grpc/blame/e699e0135e4d59e3763e7bdea5fff002cc2efab3/doc/health-checking.md#L64. 51 const serviceNameForHealthCheckGrpc = "" 52 53 type options struct { 54 client prowflagutil.KubernetesOptions 55 github prowflagutil.GitHubOptions 56 port int 57 cookiefilePath string 58 59 config configflagutil.ConfigOptions 60 61 dryRun bool 62 gracePeriod time.Duration 63 instrumentationOptions prowflagutil.InstrumentationOptions 64 } 65 66 func gatherOptions(fs *flag.FlagSet, args ...string) options { 67 var o options 68 fs.IntVar(&o.port, "port", 32000, "TCP port for gRPC.") 69 fs.BoolVar(&o.dryRun, "dry-run", true, "Dry run for testing. Uses API tokens but does not mutate.") 70 fs.DurationVar(&o.gracePeriod, "grace-period", 180*time.Second, "On shutdown, try to handle remaining events for the specified duration. ") 71 fs.StringVar(&o.cookiefilePath, "cookiefile", "", "Path to git http.cookiefile, leave empty for github or anonymous") 72 for _, group := range []flagutil.OptionGroup{&o.client, &o.github, &o.instrumentationOptions, &o.config} { 73 group.AddFlags(fs) 74 } 75 76 fs.Parse(args) 77 78 return o 79 } 80 81 func (o *options) validate() error { 82 var errs []error 83 for _, group := range []flagutil.OptionGroup{&o.client, &o.github, &o.instrumentationOptions, &o.config} { 84 if err := group.Validate(o.dryRun); err != nil { 85 errs = append(errs, err) 86 } 87 } 88 89 if o.port == o.instrumentationOptions.HealthPort { 90 errs = append(errs, fmt.Errorf("both the gRPC port and health port are using the same port number %d", o.port)) 91 } 92 93 return utilerrors.NewAggregate(errs) 94 } 95 96 // interruptableServer is a wrapper type around the gRPC server, so that we can 97 // pass it along to our own interrupts package. 98 type interruptableServer struct { 99 grpcServer *grpc.Server 100 listener net.Listener 101 port int 102 } 103 104 // Shutdown shuts down the inner gRPC server as gracefully as possible, by first 105 // invoking GracefulStop() on it. This gives the server time to try to handle 106 // things gracefully internally. However if it takes too long (if the parent 107 // context cancels us), we forcefully kill the server by calling Stop(). Stop() 108 // interrupts GracefulStop() (see 109 // https://pkg.go.dev/google.golang.org/grpc#Server.Stop). 110 func (s *interruptableServer) Shutdown(ctx context.Context) error { 111 112 gracefulStopFinished := make(chan struct{}) 113 114 go func() { 115 s.grpcServer.GracefulStop() 116 close(gracefulStopFinished) 117 }() 118 119 select { 120 case <-gracefulStopFinished: 121 return nil 122 case <-ctx.Done(): 123 s.grpcServer.Stop() 124 return ctx.Err() 125 } 126 } 127 128 func (s *interruptableServer) ListenAndServe() error { 129 logrus.Infof("serving gRPC on port %d", s.port) 130 return s.grpcServer.Serve(s.listener) 131 } 132 133 func main() { 134 logrusutil.ComponentInit() 135 136 o := gatherOptions(flag.NewFlagSet(os.Args[0], flag.ExitOnError), os.Args[1:]...) 137 if err := o.validate(); err != nil { 138 logrus.WithError(err).Fatal("Invalid options") 139 } 140 141 configAgent, err := o.config.ConfigAgent() 142 if err != nil { 143 logrus.WithError(err).Fatal("Error starting config agent.") 144 } 145 146 prowjobClient, err := o.client.ProwJobClient(configAgent.Config().ProwJobNamespace, o.dryRun) 147 if err != nil { 148 logrus.WithError(err).Fatal("unable to create prow job client") 149 } 150 151 // If we are provided credentials for Git hosts, use them. These credentials 152 // hold per-host information in them so it's safe to set them globally. 153 if o.cookiefilePath != "" { 154 cmd := exec.Command("git", "config", "--global", "http.cookiefile", o.cookiefilePath) 155 if err := cmd.Run(); err != nil { 156 logrus.WithError(err).Fatal("unable to set cookiefile") 157 } 158 } 159 160 gw := gangway.Gangway{ 161 ConfigAgent: configAgent, 162 ProwJobClient: prowjobClient, 163 } 164 165 // InRepoConfig getter. 166 if o.config.MoonrakerAddress != "" { 167 moonrakerClient, err := moonraker.NewClient(o.config.MoonrakerAddress, configAgent) 168 if err != nil { 169 logrus.WithError(err).Fatal("Error getting Moonraker client.") 170 } 171 gw.InRepoConfigGetter = moonrakerClient 172 } else { 173 gitClient, err := o.github.GitClientFactory(o.cookiefilePath, &o.config.InRepoConfigCacheDirBase, o.dryRun, false) 174 if err != nil { 175 logrus.WithError(err).Fatal("Error getting Git client.") 176 } 177 ircc, err := config.NewInRepoConfigCache(o.config.InRepoConfigCacheSize, configAgent, gitClient) 178 if err != nil { 179 logrus.WithError(err).Fatal("Error creating InRepoConfigCache.") 180 } 181 gw.InRepoConfigGetter = ircc 182 } 183 184 metrics.ExposeMetrics("gangway", configAgent.Config().PushGateway, o.instrumentationOptions.MetricsPort) 185 186 // Start serving liveness endpoint /healthz. 187 healthHTTP := pjutil.NewHealthOnPort(o.instrumentationOptions.HealthPort) 188 189 lis, err := net.Listen("tcp", ":"+strconv.Itoa(o.port)) 190 if err != nil { 191 logrus.WithError(err).Fatal("failed to set up tcp connection") 192 } 193 194 // Create a new gRPC (empty) server, and wire it up to act as a "ProwServer" 195 // as defined in the auto-generated gangway_grpc.pb.go file. Also inject an 196 // interceptor for collecting Prometheus metrics for all unary gRPC 197 // requests. 198 grpcServer := grpc.NewServer( 199 grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor), 200 ) 201 gangway.RegisterProwServer(grpcServer, &gw) 202 grpc_prometheus.Register(grpcServer) 203 204 // Create gRPC health check endpoint and add it to our server. This can be 205 // used by the ESPv2 proxy container if Gangway is deployed for Cloud 206 // Endpoints. 207 // 208 // Note that this just configures our gRPC server to be able to respond to 209 // native gRPC health check requests. The actual serving of these health 210 // check requests will only happen when we finally call 211 // interrupts.ListenAndServe() down below. 212 healthcheckGrpc := health.NewServer() 213 healthpb.RegisterHealthServer(grpcServer, healthcheckGrpc) 214 healthcheckGrpc.SetServingStatus(serviceNameForHealthCheckGrpc, healthpb.HealthCheckResponse_SERVING) 215 216 // Register reflection service on gRPC server. This enables testing through 217 // clients that don't have the generated stubs baked in, such as grpcurl. 218 reflection.Register(grpcServer) 219 220 s := &interruptableServer{ 221 grpcServer: grpcServer, 222 listener: lis, 223 port: o.port, 224 } 225 226 // Start serving readiness endpoint /healthz/ready. Note that this is a 227 // workaround for older Kubernetes clusters (older than K8s 1.24) that do 228 // not support native gRPC health checks. 229 healthHTTP.ServeReady() 230 231 // Start serving gRPC requests! Note that ListenAndServe() does not block, 232 // while WaitForGracefulShutdown() does block. 233 interrupts.ListenAndServe(s, o.gracePeriod) 234 interrupts.WaitForGracefulShutdown() 235 logrus.Info("Ended gracefully") 236 }