blob: 4d1ffc3d688cd4400dc3c5d5d3b1c7473c3bb548 [file] [log] [blame]
package db
import (
"context"
"encoding/json"
"errors"
"time"
fs "cloud.google.com/go/firestore"
"go.skia.org/infra/autoroll/go/config"
"go.skia.org/infra/go/firestore"
"go.skia.org/infra/go/skerr"
"golang.org/x/oauth2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
)
const (
// Collection name for Configs.
collectionConfigs = "Configs"
// Firestore-related constants.
defaultAttempts = 3
defaultTimeout = 10 * time.Second
)
var (
ErrNotFound = errors.New("Request with given ID does not exist.")
)
// DB provides methods for interacting with a database of Configs.
type DB interface {
// Close cleans up resources associated with the DB.
Close() error
// Get returns the Config for the given roller.
Get(ctx context.Context, rollerID string) (*config.Config, error)
// GetAll returns Configs for all known rollers.
GetAll(ctx context.Context) ([]*config.Config, error)
// Put inserts the Config into the DB. Implementations MUST validate the
// Config before inserting into the DB.
Put(ctx context.Context, rollerID string, cfg *config.Config) error
// Delete removes the Config for the given roller from the DB.
Delete(ctx context.Context, rollerID string) error
}
// firestoreB is a DB implementation backed by Firestore.
type FirestoreDB struct {
client *firestore.Client
coll *fs.CollectionRef
}
// NewDB returns a DB instance backed by the given firestore.Client.
func NewDB(ctx context.Context, client *firestore.Client) (*FirestoreDB, error) {
db := &FirestoreDB{
client: client,
coll: client.Collection(collectionConfigs),
}
return db, nil
}
// NewDBWithParams returns a DB instance backed by Firestore, using the given
// params.
func NewDBWithParams(ctx context.Context, project, namespace, instance string, ts oauth2.TokenSource) (*FirestoreDB, error) {
client, err := firestore.NewClient(ctx, project, namespace, instance, ts)
if err != nil {
return nil, skerr.Wrap(err)
}
return NewDB(ctx, client)
}
// Close implements DB.
func (d *FirestoreDB) Close() error {
return d.client.Close()
}
// Get implements DB.
func (d *FirestoreDB) Get(ctx context.Context, rollerID string) (*config.Config, error) {
ref := d.coll.Doc(rollerID)
doc, err := d.client.Get(ctx, ref, defaultAttempts, defaultTimeout)
if err != nil {
if status.Code(err) == codes.NotFound {
return nil, ErrNotFound
} else {
return nil, skerr.Wrap(err)
}
}
return decodeConfig(doc.Data())
}
// GetAll implements DB.
func (d *FirestoreDB) GetAll(ctx context.Context) ([]*config.Config, error) {
rv := []*config.Config{}
if err := d.client.IterDocs(ctx, "GetAll", "GetAll", d.coll.Query, defaultAttempts, defaultTimeout, func(doc *fs.DocumentSnapshot) error {
cfg, err := decodeConfig(doc.Data())
if err != nil {
return skerr.Wrapf(err, "failed to decode config %s", doc.Ref.Path)
}
rv = append(rv, cfg)
return nil
}); err != nil {
return nil, skerr.Wrap(err)
}
return rv, nil
}
// Put implements DB.
func (d *FirestoreDB) Put(ctx context.Context, rollerID string, cfg *config.Config) error {
if err := cfg.Validate(); err != nil {
return err
}
data, err := encodeConfig(cfg)
if err != nil {
return skerr.Wrap(err)
}
ref := d.coll.Doc(rollerID)
if _, err := ref.Set(ctx, data); err != nil {
return skerr.Wrap(err)
}
return nil
}
// Delete implements DB.
func (d *FirestoreDB) Delete(ctx context.Context, rollerID string) error {
ref := d.coll.Doc(rollerID)
if _, err := ref.Delete(ctx); err != nil {
return skerr.Wrap(err)
}
return nil
}
// encodeConfig converts the config.Config to a map[string]interface which is
// able to be stored in Firestore.
func encodeConfig(cfg *config.Config) (map[string]interface{}, error) {
b, err := protojson.Marshal(cfg)
if err != nil {
return nil, skerr.Wrap(err)
}
var rv map[string]interface{}
if err := json.Unmarshal(b, &rv); err != nil {
return nil, skerr.Wrap(err)
}
return rv, nil
}
// decodeConfig converts the map[string]interface retrieved from Firestore to a
// config.Config.
func decodeConfig(data map[string]interface{}) (*config.Config, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, skerr.Wrap(err)
}
cfg := new(config.Config)
if err := protojson.Unmarshal(b, cfg); err != nil {
rollerID, ok := data["rollerName"].(string)
if !ok {
// This shouldn't happen, but if it does we shouldn't return a
// FailedDecodeError, because the caller might try to use it.
return nil, skerr.Wrapf(err, "failed to decode config and unable to find roller ID")
}
return nil, &FailedDecodeError{
Err: skerr.Wrap(err),
RollerID: rollerID,
}
}
return cfg, nil
}
// FailedDecodeError is an Error indicating that we failed to decode the config
// for a roller.
type FailedDecodeError struct {
Err error
RollerID string
}
// Error implements error.
func (e *FailedDecodeError) Error() string {
return e.Err.Error()
}
// IsFailedDecode determines whether the error is a failure to decode the config
// for a roller, and if so returns the ID of the roller and true.
func IsFailedDecode(err error) (string, bool) {
e, ok := skerr.Unwrap(err).(*FailedDecodeError)
if ok {
return e.RollerID, true
}
return "", false
}