go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/frontend/main.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package main is the main entry point for the app.
    16  package main
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"io"
    22  
    23  	"go.chromium.org/luci/auth/identity"
    24  	"go.chromium.org/luci/common/data/stringset"
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/common/logging"
    27  	"go.chromium.org/luci/common/retry/transient"
    28  	"go.chromium.org/luci/config/server/cfgmodule"
    29  	"go.chromium.org/luci/gae/filter/dscache"
    30  	"go.chromium.org/luci/gae/service/datastore"
    31  	"go.chromium.org/luci/grpc/prpc"
    32  	"go.chromium.org/luci/server"
    33  	"go.chromium.org/luci/server/auth"
    34  	"go.chromium.org/luci/server/auth/openid"
    35  	"go.chromium.org/luci/server/bqlog"
    36  	"go.chromium.org/luci/server/cron"
    37  	"go.chromium.org/luci/server/encryptedcookies"
    38  	"go.chromium.org/luci/server/gaeemulation"
    39  	"go.chromium.org/luci/server/gerritauth"
    40  	"go.chromium.org/luci/server/module"
    41  	"go.chromium.org/luci/server/redisconn"
    42  	"go.chromium.org/luci/server/router"
    43  	"go.chromium.org/luci/server/secrets"
    44  	"go.chromium.org/luci/server/tq"
    45  
    46  	// Store auth sessions in the datastore.
    47  	_ "go.chromium.org/luci/server/encryptedcookies/session/datastore"
    48  	// Enable datastore transactional tasks support.
    49  	_ "go.chromium.org/luci/server/tq/txn/datastore"
    50  
    51  	"go.chromium.org/luci/buildbucket/appengine/internal/buildcron"
    52  	"go.chromium.org/luci/buildbucket/appengine/internal/buildercron"
    53  	"go.chromium.org/luci/buildbucket/appengine/internal/clients"
    54  	"go.chromium.org/luci/buildbucket/appengine/internal/config"
    55  	"go.chromium.org/luci/buildbucket/appengine/internal/metrics"
    56  	"go.chromium.org/luci/buildbucket/appengine/internal/redirect"
    57  	"go.chromium.org/luci/buildbucket/appengine/rpc"
    58  	"go.chromium.org/luci/buildbucket/appengine/tasks"
    59  	pb "go.chromium.org/luci/buildbucket/proto"
    60  )
    61  
    62  var cacheEnabled = stringset.NewFromSlice("Project", "BuildStatus")
    63  
    64  func handlePubSubMessage(ctx *router.Context, identity identity.Identity, handler func(context.Context, io.Reader) error) {
    65  	if got := auth.CurrentIdentity(ctx.Request.Context()); got != identity {
    66  		logging.Errorf(ctx.Request.Context(), "Expecting ID token of %q, got %q", identity, got)
    67  		ctx.Writer.WriteHeader(403)
    68  	} else {
    69  		switch err := handler(ctx.Request.Context(), ctx.Request.Body); {
    70  		case err == nil:
    71  			ctx.Writer.WriteHeader(200)
    72  		case transient.Tag.In(err):
    73  			logging.Warningf(ctx.Request.Context(), "Encounter transient error when processing pubsub msg: %s", err)
    74  			ctx.Writer.WriteHeader(500) // PubSub will resend this msg.
    75  		default:
    76  			logging.Errorf(ctx.Request.Context(), "Encounter non-transient error when processing pubsub msg: %s", err)
    77  			ctx.Writer.WriteHeader(202)
    78  		}
    79  	}
    80  }
    81  
    82  func main() {
    83  	mods := []module.Module{
    84  		bqlog.NewModuleFromFlags(),
    85  		cfgmodule.NewModuleFromFlags(),
    86  		cron.NewModuleFromFlags(),
    87  		encryptedcookies.NewModuleFromFlags(), // Required for auth sessions.
    88  		gaeemulation.NewModuleFromFlags(),
    89  		gerritauth.NewModuleFromFlags(),
    90  		tq.NewModuleFromFlags(),
    91  		redisconn.NewModuleFromFlags(),
    92  		secrets.NewModuleFromFlags(),
    93  	}
    94  
    95  	server.Main(nil, mods, func(srv *server.Server) error {
    96  		o := srv.Options
    97  		srv.Context = metrics.WithServiceInfo(srv.Context, o.TsMonServiceName, o.TsMonJobName, o.Hostname)
    98  
    99  		// Install a global bigquery client.
   100  		bqClient, err := clients.NewBqClient(srv.Context, o.CloudProject)
   101  		if err != nil {
   102  			return errors.Annotate(err, "failed to initiate the global Bigquery client").Err()
   103  		}
   104  		srv.Context = clients.WithBqClient(srv.Context, bqClient)
   105  
   106  		// Enable dscache on Project entities only. Other datastore entities aren't
   107  		// ready.
   108  		srv.Context = dscache.AddShardFunctions(srv.Context, func(k *datastore.Key) (shards int, ok bool) {
   109  			if cacheEnabled.Has(k.Kind()) {
   110  				return 1, true
   111  			}
   112  			return 0, true
   113  		})
   114  
   115  		srv.SetRPCAuthMethods([]auth.Method{
   116  			// OpenID Connect tokens are the prefered auth method.
   117  			//
   118  			// However, this method must be first because GoogleOAuth2Method doesn't
   119  			// know how to ignore a JWT in the Authorization header.
   120  			//
   121  			// This method does not interfere with gerritauth, however, because
   122  			// gerritauth looks at a separate header (usually "X-Gerrit-Auth").
   123  			&openid.GoogleIDTokenAuthMethod{
   124  				AudienceCheck: openid.AudienceMatchesHost,
   125  
   126  				// This is true to also allow GoogleOAuth2Method - if GoogleOAuth2Method
   127  				// is removed then this should be removed as well.
   128  				SkipNonJWT: true,
   129  			},
   130  
   131  			// This method is ~deprecated, but is still used by the majority of
   132  			// clients as of this CL.
   133  			//
   134  			// This is because the OAuth2/OpenID split complicates clients because
   135  			// they need to know when to use OAuth2 vs OpenID. See additional details:
   136  			// https://pkg.go.dev/go.chromium.org/luci/server/auth#GoogleOAuth2Method
   137  			&auth.GoogleOAuth2Method{
   138  				Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
   139  			},
   140  
   141  			// For authenticating calls from Gerrit plugins.
   142  			&gerritauth.Method,
   143  		})
   144  
   145  		srv.ConfigurePRPC(func(p *prpc.Server) {
   146  			// Allow cross-origin calls, in particular calls using Gerrit auth
   147  			// headers.
   148  			p.AccessControl = func(context.Context, string) prpc.AccessControlDecision {
   149  				return prpc.AccessControlDecision{
   150  					AllowCrossOriginRequests: true,
   151  					AllowCredentials:         true,
   152  					AllowHeaders:             []string{gerritauth.Method.Header},
   153  				}
   154  			}
   155  			// TODO(crbug/1082369): Remove this workaround once field masks can be
   156  			// decoded.
   157  			p.HackFixFieldMasksForJSON = true
   158  		})
   159  
   160  		pb.RegisterBuildsServer(srv, rpc.NewBuilds())
   161  		pb.RegisterBuildersServer(srv, rpc.NewBuilders())
   162  
   163  		cron.RegisterHandler("delete_builds", buildcron.DeleteOldBuilds)
   164  		cron.RegisterHandler("expire_builds", buildcron.TimeoutExpiredBuilds)
   165  		cron.RegisterHandler("sync_backend_tasks", buildcron.TriggerSyncBackendTasks)
   166  		cron.RegisterHandler("update_config", config.UpdateSettingsCfg)
   167  		cron.RegisterHandler("update_project_config", config.UpdateProjectCfg)
   168  		cron.RegisterHandler("reset_expired_leases", buildcron.ResetExpiredLeases)
   169  		cron.RegisterHandler("remove_inactive_builder_stats", buildercron.RemoveInactiveBuilderStats)
   170  		redirect.InstallHandlers(srv.Routes, router.NewMiddlewareChain(auth.Authenticate(srv.CookieAuth)))
   171  
   172  		// PubSub push handler processing messages
   173  		oidcMW := router.NewMiddlewareChain(
   174  			auth.Authenticate(&openid.GoogleIDTokenAuthMethod{
   175  				AudienceCheck: openid.AudienceMatchesHost,
   176  			}),
   177  		)
   178  		// swarming-go-pubsub@ is a part of the PubSub Push subscription config.
   179  		swarmingPusherID := identity.Identity(fmt.Sprintf("user:swarming-go-pubsub@%s.iam.gserviceaccount.com", srv.Options.CloudProject))
   180  		srv.Routes.POST("/push-handlers/swarming-go/notify", oidcMW, func(ctx *router.Context) {
   181  			handlePubSubMessage(ctx, swarmingPusherID, tasks.SubNotify)
   182  		})
   183  
   184  		// task-backend-update-task-push@ is a part of the PubSub Push subscription config.
   185  		taskBackendPusherID := identity.Identity(fmt.Sprintf("user:task-backend-update-task-push@%s.iam.gserviceaccount.com", srv.Options.CloudProject))
   186  		srv.Routes.POST("/internal/pubsub/backend/update-build-task", oidcMW, func(ctx *router.Context) {
   187  			handlePubSubMessage(ctx, taskBackendPusherID, tasks.UpdateBuildTask)
   188  		})
   189  
   190  		return nil
   191  	})
   192  }