docs/server/storage/rethinkdb.go

281 lines
8.8 KiB
Go

package storage
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"time"
"github.com/docker/notary"
"github.com/docker/notary/storage/rethinkdb"
"github.com/docker/notary/tuf/data"
"gopkg.in/dancannon/gorethink.v2"
)
// RDBTUFFile is a tuf file record
type RDBTUFFile struct {
rethinkdb.Timing
GunRoleVersion []interface{} `gorethink:"gun_role_version"`
Gun string `gorethink:"gun"`
Role string `gorethink:"role"`
Version int `gorethink:"version"`
Sha256 string `gorethink:"sha256"`
Data []byte `gorethink:"data"`
TSchecksum string `gorethink:"timestamp_checksum"`
}
// TableName returns the table name for the record type
func (r RDBTUFFile) TableName() string {
return "tuf_files"
}
// RDBKey is the public key record
type RDBKey struct {
rethinkdb.Timing
Gun string `gorethink:"gun"`
Role string `gorethink:"role"`
Cipher string `gorethink:"cipher"`
Public []byte `gorethink:"public"`
}
// TableName returns the table name for the record type
func (r RDBKey) TableName() string {
return "tuf_keys"
}
// RethinkDB implements a MetaStore against the Rethink Database
type RethinkDB struct {
dbName string
sess *gorethink.Session
}
// NewRethinkDBStorage initializes a RethinkDB object
func NewRethinkDBStorage(dbName string, sess *gorethink.Session) RethinkDB {
return RethinkDB{
dbName: dbName,
sess: sess,
}
}
// GetKey returns the cipher and public key for the given GUN and role.
// If the GUN+role don't exist, returns an error.
func (rdb RethinkDB) GetKey(gun, role string) (cipher string, public []byte, err error) {
var key RDBKey
res, err := gorethink.DB(rdb.dbName).Table(key.TableName()).GetAllByIndex(
rdbGunRoleIdx, []string{gun, role},
).Run(rdb.sess)
if err != nil {
return "", nil, err
}
defer res.Close()
err = res.One(&key)
if err == gorethink.ErrEmptyResult {
return "", nil, &ErrNoKey{gun: gun}
}
return key.Cipher, key.Public, err
}
// SetKey sets the cipher and public key for the given GUN and role if
// it doesn't already exist. Otherwise an error is returned.
func (rdb RethinkDB) SetKey(gun, role, cipher string, public []byte) error {
now := time.Now()
key := RDBKey{
Timing: rethinkdb.Timing{
CreatedAt: now,
UpdatedAt: now,
},
Gun: gun,
Role: role,
Cipher: cipher,
Public: public,
}
_, err := gorethink.DB(rdb.dbName).Table(key.TableName()).Insert(key).RunWrite(rdb.sess)
return err
}
// UpdateCurrent adds new metadata version for the given GUN if and only
// if it's a new role, or the version is greater than the current version
// for the role. Otherwise an error is returned.
func (rdb RethinkDB) UpdateCurrent(gun string, update MetaUpdate) error {
now := time.Now()
checksum := sha256.Sum256(update.Data)
file := RDBTUFFile{
Timing: rethinkdb.Timing{
CreatedAt: now,
UpdatedAt: now,
},
GunRoleVersion: []interface{}{gun, update.Role, update.Version},
Gun: gun,
Role: update.Role,
Version: update.Version,
Sha256: hex.EncodeToString(checksum[:]),
Data: update.Data,
}
_, err := gorethink.DB(rdb.dbName).Table(file.TableName()).Insert(
file,
gorethink.InsertOpts{
Conflict: "error", // default but explicit for clarity of intent
},
).RunWrite(rdb.sess)
if err != nil && gorethink.IsConflictErr(err) {
return &ErrOldVersion{}
}
return err
}
// UpdateCurrentWithTSChecksum adds new metadata version for the given GUN with an associated
// checksum for the timestamp it belongs to, to afford us transaction-like functionality
func (rdb RethinkDB) UpdateCurrentWithTSChecksum(gun, tsChecksum string, update MetaUpdate) error {
now := time.Now()
checksum := sha256.Sum256(update.Data)
file := RDBTUFFile{
Timing: rethinkdb.Timing{
CreatedAt: now,
UpdatedAt: now,
},
GunRoleVersion: []interface{}{gun, update.Role, update.Version},
Gun: gun,
Role: update.Role,
Version: update.Version,
Sha256: hex.EncodeToString(checksum[:]),
TSchecksum: tsChecksum,
Data: update.Data,
}
_, err := gorethink.DB(rdb.dbName).Table(file.TableName()).Insert(
file,
gorethink.InsertOpts{
Conflict: "error", // default but explicit for clarity of intent
},
).RunWrite(rdb.sess)
if err != nil && gorethink.IsConflictErr(err) {
return &ErrOldVersion{}
}
return err
}
// Used for sorting updates alphabetically by role name, such that timestamp is always last:
// Ordering: root, snapshot, targets, targets/* (delegations), timestamp
type updateSorter []MetaUpdate
func (u updateSorter) Len() int { return len(u) }
func (u updateSorter) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
func (u updateSorter) Less(i, j int) bool {
return u[i].Role < u[j].Role
}
// UpdateMany adds multiple new metadata for the given GUN. RethinkDB does
// not support transactions, therefore we will attempt to insert the timestamp
// last as this represents a published version of the repo. However, we will
// insert all other role data in alphabetical order first, and also include the
// associated timestamp checksum so that we can easily roll back this pseudotransaction
func (rdb RethinkDB) UpdateMany(gun string, updates []MetaUpdate) error {
// find the timestamp first and save its checksum
// then apply the updates in alphabetic role order with the timestamp last
// if there are any failures, we roll back in the same alphabetic order
var tsChecksum string
for _, up := range updates {
if up.Role == data.CanonicalTimestampRole {
tsChecksumBytes := sha256.Sum256(up.Data)
tsChecksum = hex.EncodeToString(tsChecksumBytes[:])
break
}
}
// alphabetize the updates by Role name
sort.Stable(updateSorter(updates))
for _, up := range updates {
if err := rdb.UpdateCurrentWithTSChecksum(gun, tsChecksum, up); err != nil {
// roll back with best-effort deletion, and then error out
rdb.deleteByTSChecksum(tsChecksum)
return err
}
}
return nil
}
// GetCurrent returns the modification date and data part of the metadata for
// the latest version of the given GUN and role. If there is no data for
// the given GUN and role, an error is returned.
func (rdb RethinkDB) GetCurrent(gun, role string) (created *time.Time, data []byte, err error) {
file := RDBTUFFile{}
res, err := gorethink.DB(rdb.dbName).Table(file.TableName(), gorethink.TableOpts{ReadMode: "majority"}).GetAllByIndex(
rdbGunRoleIdx, []string{gun, role},
).OrderBy(gorethink.Desc("version")).Run(rdb.sess)
if err != nil {
return nil, nil, err
}
defer res.Close()
if res.IsNil() {
return nil, nil, ErrNotFound{}
}
err = res.One(&file)
if err == gorethink.ErrEmptyResult {
return nil, nil, ErrNotFound{}
}
return &file.CreatedAt, file.Data, err
}
// GetChecksum returns the given TUF role file and creation date for the
// GUN with the provided checksum. If the given (gun, role, checksum) are
// not found, it returns storage.ErrNotFound
func (rdb RethinkDB) GetChecksum(gun, role, checksum string) (created *time.Time, data []byte, err error) {
var file RDBTUFFile
res, err := gorethink.DB(rdb.dbName).Table(file.TableName(), gorethink.TableOpts{ReadMode: "majority"}).GetAllByIndex(
rdbGunRoleSha256Idx, []string{gun, role, checksum},
).Run(rdb.sess)
if err != nil {
return nil, nil, err
}
defer res.Close()
if res.IsNil() {
return nil, nil, ErrNotFound{}
}
err = res.One(&file)
if err == gorethink.ErrEmptyResult {
return nil, nil, ErrNotFound{}
}
return &file.CreatedAt, file.Data, err
}
// Delete removes all metadata for a given GUN. It does not return an
// error if no metadata exists for the given GUN.
func (rdb RethinkDB) Delete(gun string) error {
_, err := gorethink.DB(rdb.dbName).Table(RDBTUFFile{}.TableName()).GetAllByIndex(
"gun", []string{gun},
).Delete().RunWrite(rdb.sess)
if err != nil {
return fmt.Errorf("unable to delete %s from database: %s", gun, err.Error())
}
return nil
}
// deleteByTSChecksum removes all metadata by a timestamp checksum, used for rolling back a "transaction"
// from a call to rethinkdb's UpdateMany
func (rdb RethinkDB) deleteByTSChecksum(tsChecksum string) error {
_, err := gorethink.DB(rdb.dbName).Table(RDBTUFFile{}.TableName()).GetAllByIndex(
"timestamp_checksum", []string{tsChecksum},
).Delete().RunWrite(rdb.sess)
if err != nil {
return fmt.Errorf("unable to delete timestamp checksum data: %s from database: %s", tsChecksum, err.Error())
}
return nil
}
// Bootstrap sets up the database and tables, also creating the notary server user with appropriate db permission
func (rdb RethinkDB) Bootstrap() error {
if err := rethinkdb.SetupDB(rdb.sess, rdb.dbName, []rethinkdb.Table{
tufFiles,
keys,
}); err != nil {
return err
}
return rethinkdb.CreateAndGrantDBUser(rdb.sess, rdb.dbName, notary.NotaryServerUser, "")
}
// CheckHealth is currently a noop
func (rdb RethinkDB) CheckHealth() error {
return nil
}