blob: 9c9ed8195d680e6124fca2f86c001a6bb21a0759 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package gaeemulation provides a server module that adds implementation of
// some https://godoc.org/go.chromium.org/gae APIs to the global server context.
//
// The implementation is based on regular Cloud APIs and works from anywhere
// (not necessarily from Appengine).
//
// Usage:
//
// func main() {
// modules := []module.Module{
// gaeemulation.NewModuleFromFlags(),
// }
// server.Main(nil, modules, func(srv *server.Server) error {
// srv.Routes.GET("/", ..., func(c *router.Context) {
// ent := Entity{ID: "..."}
// err := datastore.Get(c.Context, &ent)
// ...
// })
// return nil
// })
// }
//
// TODO(vadimsh): Currently provides datastore API only.
package gaeemulation
import (
"context"
"flag"
"os"
"cloud.google.com/go/datastore"
"google.golang.org/api/option"
"go.chromium.org/gae/impl/cloud"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/module"
)
// ModuleOptions are empty for now but exist to make the gaeemulation interface
// similar to interfaces of other modules.
type ModuleOptions struct{}
// Register registers the command line flags.
func (o *ModuleOptions) Register(f *flag.FlagSet) {}
// NewModule returns a server module that adds implementation of
// some https://godoc.org/go.chromium.org/gae APIs to the global server context.
func NewModule(opts *ModuleOptions) module.Module {
if opts == nil {
opts = &ModuleOptions{}
}
return &gaeModule{opts: opts}
}
// NewModuleFromFlags is a variant of NewModule that initializes options through
// command line flags.
//
// Calling this function registers flags in flag.CommandLine. They are usually
// parsed in server.Main(...).
func NewModuleFromFlags() module.Module {
opts := &ModuleOptions{}
opts.Register(flag.CommandLine)
return NewModule(opts)
}
// gaeModule implements module.Module.
type gaeModule struct {
opts *ModuleOptions
}
// Name is part of module.Module interface.
func (*gaeModule) Name() string {
return "go.chromium.org/luci/server/gaeemulation"
}
// Initialize is part of module.Module interface.
func (m *gaeModule) Initialize(ctx context.Context, host module.Host, opts module.HostOptions) (context.Context, error) {
var client *datastore.Client
if opts.CloudProject != "" {
var err error
if client, err = m.initDSClient(ctx, host, opts.CloudProject); err != nil {
return nil, err
}
}
cfg := &cloud.ConfigLite{
IsDev: !opts.Prod,
ProjectID: opts.CloudProject,
DS: client, // if nil, datastore calls will fail gracefully(-ish)
}
return cfg.Use(ctx), nil
}
// initDSClient sets up Cloud Datastore client that uses AsSelf server token
// source.
func (m *gaeModule) initDSClient(ctx context.Context, host module.Host, cloudProject string) (*datastore.Client, error) {
logging.Infof(ctx, "Setting up datastore client for project %q", cloudProject)
// Enable auth only when using the real datastore.
var clientOpts []option.ClientOption
if addr := os.Getenv("DATASTORE_EMULATOR_HOST"); addr == "" {
ts, err := auth.GetTokenSource(ctx, auth.AsSelf, auth.WithScopes(auth.CloudOAuthScopes...))
if err != nil {
return nil, errors.Annotate(err, "failed to initialize the token source").Err()
}
clientOpts = []option.ClientOption{option.WithTokenSource(ts)}
}
client, err := datastore.NewClient(ctx, cloudProject, clientOpts...)
if err != nil {
return nil, errors.Annotate(err, "failed to instantiate the datastore client").Err()
}
host.RegisterCleanup(func(ctx context.Context) {
if err := client.Close(); err != nil {
logging.Warningf(ctx, "Failed to close the datastore client - %s", err)
}
})
// TODO(vadimsh): "Ping" the datastore to verify the credentials are correct?
return client, nil
}