go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/gaeemulation/module.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 gaeemulation provides a server module that adds implementation of 16 // some https://godoc.org/go.chromium.org/luci/gae APIs to the global server context. 17 // 18 // The implementation is based on regular Cloud APIs and works from anywhere 19 // (not necessarily from Appengine). 20 // 21 // Usage: 22 // 23 // func main() { 24 // modules := []module.Module{ 25 // gaeemulation.NewModuleFromFlags(), 26 // } 27 // server.Main(nil, modules, func(srv *server.Server) error { 28 // srv.Routes.GET("/", ..., func(c *router.Context) { 29 // ent := Entity{ID: "..."} 30 // err := datastore.Get(c.Context, &ent) 31 // ... 32 // }) 33 // return nil 34 // }) 35 // } 36 // 37 // TODO(vadimsh): Currently provides datastore API only. 38 package gaeemulation 39 40 import ( 41 "context" 42 "flag" 43 "os" 44 45 "cloud.google.com/go/datastore" 46 "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 47 "google.golang.org/api/option" 48 "google.golang.org/grpc" 49 50 "go.chromium.org/luci/appengine/gaesecrets" 51 "go.chromium.org/luci/common/errors" 52 "go.chromium.org/luci/common/logging" 53 "go.chromium.org/luci/gae/filter/dscache" 54 "go.chromium.org/luci/gae/filter/txndefer" 55 "go.chromium.org/luci/gae/impl/cloud" 56 "go.chromium.org/luci/grpc/grpcmon" 57 58 "go.chromium.org/luci/server/auth" 59 "go.chromium.org/luci/server/module" 60 "go.chromium.org/luci/server/redisconn" 61 "go.chromium.org/luci/server/secrets" 62 ) 63 64 // ModuleName can be used to refer to this module when declaring dependencies. 65 var ModuleName = module.RegisterName("go.chromium.org/luci/server/gaeemulation") 66 67 // ModuleOptions contain configuration of the GAE Emulation server module 68 type ModuleOptions struct { 69 DSCache string // currently either "disable" (default) or "redis" 70 RandomSecretsInDatastore bool // true to replace the random secrets store with the GAEv1-one 71 DSConnectionPoolSize int // passed to WithGRPCConnectionPool, if > 0. 72 } 73 74 // Register registers the command line flags. 75 func (o *ModuleOptions) Register(f *flag.FlagSet) { 76 f.StringVar( 77 &o.DSCache, 78 "ds-cache", 79 o.DSCache, 80 `What datastore caching layer to use ("disable" or "redis").`, 81 ) 82 f.BoolVar( 83 &o.RandomSecretsInDatastore, 84 "random-secrets-in-datastore", 85 o.RandomSecretsInDatastore, 86 `If set, use datastore to store random secrets instead of deriving them from a -root-secret. `+ 87 `Can be used for compatibility with older GAE services. Do not use in new services.`, 88 ) 89 f.IntVar( 90 &o.DSConnectionPoolSize, 91 "ds-connection-pool-size", 92 o.DSConnectionPoolSize, 93 "If set, DS client is constructed with WithGRPCConnectionPool() and this value. ", 94 ) 95 } 96 97 // NewModule returns a server module that adds implementation of 98 // some https://godoc.org/go.chromium.org/luci/gae APIs to the global server 99 // context. 100 func NewModule(opts *ModuleOptions) module.Module { 101 if opts == nil { 102 opts = &ModuleOptions{} 103 } 104 return &gaeModule{opts: opts} 105 } 106 107 // NewModuleFromFlags is a variant of NewModule that initializes options through 108 // command line flags. 109 // 110 // Calling this function registers flags in flag.CommandLine. They are usually 111 // parsed in server.Main(...). 112 func NewModuleFromFlags() module.Module { 113 opts := &ModuleOptions{} 114 opts.Register(flag.CommandLine) 115 return NewModule(opts) 116 } 117 118 // gaeModule implements module.Module. 119 type gaeModule struct { 120 opts *ModuleOptions 121 } 122 123 // Name is part of module.Module interface. 124 func (*gaeModule) Name() module.Name { 125 return ModuleName 126 } 127 128 // Dependencies is part of module.Module interface. 129 func (*gaeModule) Dependencies() []module.Dependency { 130 return []module.Dependency{ 131 module.OptionalDependency(redisconn.ModuleName), // for dscache, if enabled 132 module.OptionalDependency(secrets.ModuleName), // to install DS random secrets backend 133 } 134 } 135 136 // Initialize is part of module.Module interface. 137 func (m *gaeModule) Initialize(ctx context.Context, host module.Host, opts module.HostOptions) (context.Context, error) { 138 // Use zstd in luci/server's dscache. It will be default at some point. 139 dscache.UseZstd = true 140 141 var cacheImpl dscache.Cache 142 switch m.opts.DSCache { 143 case "", "disable": 144 // don't use caching 145 case "redis": 146 pool := redisconn.GetPool(ctx) 147 if pool == nil { 148 return nil, errors.Reason("can't use `-ds-cache redis`: redisconn module is not configured").Err() 149 } 150 cacheImpl = redisCache{pool: pool} 151 default: 152 return nil, errors.Reason("unsupported -ds-cache %q", m.opts.DSCache).Err() 153 } 154 155 if m.opts.RandomSecretsInDatastore { 156 store, _ := secrets.CurrentStore(ctx).(*secrets.SecretManagerStore) 157 if store == nil { 158 return nil, errors.Reason("-random-secrets-in-datastore requires module %q", secrets.ModuleName).Err() 159 } 160 store.SetRandomSecretsStore(gaesecrets.New(nil)) 161 } 162 163 if s := m.opts.DSConnectionPoolSize; s < 0 { 164 return nil, errors.Reason("-ds-connection-pool-size: must be >= 0, but %d", s).Err() 165 } 166 167 var client *datastore.Client 168 if opts.CloudProject != "" { 169 var err error 170 if client, err = m.initDSClient(ctx, host, opts.CloudProject, m.opts.DSConnectionPoolSize); err != nil { 171 return nil, err 172 } 173 } 174 cfg := &cloud.ConfigLite{ 175 IsDev: !opts.Prod, 176 ProjectID: opts.CloudProject, 177 DS: client, // if nil, datastore calls will fail gracefully(-ish) 178 } 179 180 ctx = cfg.Use(ctx) 181 if cacheImpl != nil { 182 ctx = dscache.FilterRDS(ctx, cacheImpl) 183 } 184 return txndefer.FilterRDS(ctx), nil 185 } 186 187 // initDSClient sets up Cloud Datastore client that uses AsSelf server token 188 // source. 189 func (m *gaeModule) initDSClient(ctx context.Context, host module.Host, cloudProject string, poolSize int) (*datastore.Client, error) { 190 logging.Infof(ctx, "Setting up datastore client for project %q", cloudProject) 191 192 // Enable auth only when using the real datastore. 193 var clientOpts []option.ClientOption 194 if addr := os.Getenv("DATASTORE_EMULATOR_HOST"); addr == "" { 195 ts, err := auth.GetTokenSource(ctx, auth.AsSelf, auth.WithScopes(auth.CloudOAuthScopes...)) 196 if err != nil { 197 return nil, errors.Annotate(err, "failed to initialize the token source").Err() 198 } 199 clientOpts = []option.ClientOption{ 200 option.WithTokenSource(ts), 201 option.WithGRPCDialOption(grpc.WithStatsHandler(&grpcmon.ClientRPCStatsMonitor{})), 202 option.WithGRPCDialOption(grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor())), 203 option.WithGRPCDialOption(grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor())), 204 } 205 } 206 207 if poolSize > 0 { 208 clientOpts = append(clientOpts, option.WithGRPCConnectionPool(poolSize)) 209 } 210 client, err := datastore.NewClient(ctx, cloudProject, clientOpts...) 211 if err != nil { 212 return nil, errors.Annotate(err, "failed to instantiate the datastore client").Err() 213 } 214 215 host.RegisterCleanup(func(ctx context.Context) { 216 if err := client.Close(); err != nil { 217 logging.Warningf(ctx, "Failed to close the datastore client - %s", err) 218 } 219 }) 220 221 // TODO(vadimsh): "Ping" the datastore to verify the credentials are correct? 222 223 return client, nil 224 }