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 }