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  }