blob: 896b3ed72d2183594899ad9c99287b938de42a6f [file] [log] [blame]
package db
import (
"context"
"fmt"
"strings"
"time"
"golang.org/x/oauth2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"go.skia.org/infra/go/firestore"
"go.skia.org/infra/go/skerr"
)
const (
// For accessing Firestore.
defaultAttempts = 3
getSingleTimeout = 10 * time.Second
putSingleTimeout = 10 * time.Second
// Gerrit cherrypicks collection name.
cherrypickDataCol = "CherrypickData"
)
// FirestoreDB uses Cloud Firestore for storage.
type FirestoreDB struct {
client *firestore.Client
}
// CherrypickData is the type that will be stored in FirestoreDB.
type CherrypickData struct {
Created time.Time `firestore:"created"`
ChangeNumber int64 `firestore:"change_number"`
}
// New returns an instance of FirestoreDB.
func New(ctx context.Context, ts oauth2.TokenSource, fsNamespace, fsProjectId string) (*FirestoreDB, error) {
// Instantiate firestore.
fsClient, err := firestore.NewClient(ctx, fsProjectId, "cherrypick-watcher", fsNamespace, ts)
if err != nil {
return nil, skerr.Wrapf(err, "could not init firestore")
}
return &FirestoreDB{
client: fsClient,
}, nil
}
// GetKey sanitizes the source/target repo+branch with the
// change_num to construct an ID appropriate for Firestore.
// Eg: sourceRepo=skia, sourceBranch=chrome/m100, targetRepo=skia,
// targetBranch=android/next-release and changeNum=12 will return
// "skia-chrome-m100_skia-android-next-release_12".
func GetKey(sourceRepo, sourceBranch, targetRepo, targetBranch string, changeNum int64) string {
return fmt.Sprintf("%s-%s_%s-%s_%d", sourceRepo, strings.Replace(sourceBranch, "/", "-", -1), targetRepo, strings.Replace(targetBranch, "/", "-", -1), changeNum)
}
// GetFromDB returns a CherrypickData document snapshot from Firestore. If the document is not
// found then (nil, nil) is returned.
func (f *FirestoreDB) GetFromDB(ctx context.Context, key string) (*CherrypickData, error) {
docRef := f.client.Collection(cherrypickDataCol).Doc(key)
doc, err := f.client.Get(ctx, docRef, defaultAttempts, putSingleTimeout)
if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
return nil, nil
}
if err != nil {
return nil, skerr.Wrapf(err, "could not get %s from DB", key)
}
cherrypickData := CherrypickData{}
if err := doc.DataTo(&cherrypickData); err != nil {
return nil, err
}
return &cherrypickData, nil
}
// PutInDB puts the specified CherrypickData into the DB.
func (f *FirestoreDB) PutInDB(ctx context.Context, key string, changeNum int64) error {
now := time.Now()
qd := &CherrypickData{
Created: now,
ChangeNumber: changeNum,
}
cherrypickCol := f.client.Collection(cherrypickDataCol)
_, createErr := f.client.Create(ctx, cherrypickCol.Doc(key), qd, defaultAttempts, putSingleTimeout)
if st, ok := status.FromError(createErr); ok && st.Code() == codes.AlreadyExists {
return skerr.Wrapf(createErr, "%s already exists in firestore", key)
}
if createErr != nil {
return createErr
}
return nil
}