mirror of https://github.com/docker/docs.git
214 lines
5.7 KiB
Go
214 lines
5.7 KiB
Go
package storage
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/go-sql-driver/mysql"
|
|
"github.com/jinzhu/gorm"
|
|
)
|
|
|
|
// SQLStorage implements a versioned store using a relational database.
|
|
// See server/storage/models.go
|
|
type SQLStorage struct {
|
|
gorm.DB
|
|
}
|
|
|
|
// NewSQLStorage is a convenience method to create a SQLStorage
|
|
func NewSQLStorage(dialect string, args ...interface{}) (*SQLStorage, error) {
|
|
gormDB, err := gorm.Open(dialect, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &SQLStorage{
|
|
DB: gormDB,
|
|
}, nil
|
|
}
|
|
|
|
// translateOldVersionError captures DB errors, and attempts to translate
|
|
// duplicate entry - currently only supports MySQL and Sqlite3
|
|
func translateOldVersionError(err error) error {
|
|
switch err := err.(type) {
|
|
case *mysql.MySQLError:
|
|
// https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
|
|
// 1022 = Can't write; duplicate key in table '%s'
|
|
// 1062 = Duplicate entry '%s' for key %d
|
|
if err.Number == 1022 || err.Number == 1062 {
|
|
return &ErrOldVersion{}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// UpdateCurrent updates a single TUF.
|
|
func (db *SQLStorage) UpdateCurrent(gun string, update MetaUpdate) error {
|
|
// ensure we're not inserting an immediately old version - can't use the
|
|
// struct, because that only works with non-zero values, and Version
|
|
// can be 0.
|
|
exists := db.Where("gun = ? and role = ? and version >= ?",
|
|
gun, update.Role, update.Version).First(&TUFFile{})
|
|
|
|
if !exists.RecordNotFound() {
|
|
return &ErrOldVersion{}
|
|
}
|
|
checksum := sha256.Sum256(update.Data)
|
|
return translateOldVersionError(db.Create(&TUFFile{
|
|
Gun: gun,
|
|
Role: update.Role,
|
|
Version: update.Version,
|
|
Sha256: hex.EncodeToString(checksum[:]),
|
|
Data: update.Data,
|
|
}).Error)
|
|
}
|
|
|
|
// UpdateMany atomically updates many TUF records in a single transaction
|
|
func (db *SQLStorage) UpdateMany(gun string, updates []MetaUpdate) error {
|
|
tx := db.Begin()
|
|
if tx.Error != nil {
|
|
return tx.Error
|
|
}
|
|
|
|
rollback := func(err error) error {
|
|
if rxErr := tx.Rollback().Error; rxErr != nil {
|
|
logrus.Error("Failed on Tx rollback with error: ", rxErr.Error())
|
|
return rxErr
|
|
}
|
|
return err
|
|
}
|
|
|
|
var (
|
|
query *gorm.DB
|
|
added = make(map[uint]bool)
|
|
)
|
|
for _, update := range updates {
|
|
// This looks like the same logic as UpdateCurrent, but if we just
|
|
// called, version ordering in the updates list must be enforced
|
|
// (you cannot insert the version 2 before version 1). And we do
|
|
// not care about monotonic ordering in the updates.
|
|
query = db.Where("gun = ? and role = ? and version >= ?",
|
|
gun, update.Role, update.Version).First(&TUFFile{})
|
|
|
|
if !query.RecordNotFound() {
|
|
return rollback(&ErrOldVersion{})
|
|
}
|
|
|
|
var row TUFFile
|
|
checksum := sha256.Sum256(update.Data)
|
|
hexChecksum := hex.EncodeToString(checksum[:])
|
|
query = tx.Where(map[string]interface{}{
|
|
"gun": gun,
|
|
"role": update.Role,
|
|
"version": update.Version,
|
|
}).Attrs("data", update.Data).Attrs("sha256", hexChecksum).FirstOrCreate(&row)
|
|
|
|
if query.Error != nil {
|
|
return rollback(translateOldVersionError(query.Error))
|
|
}
|
|
// it's previously been added, which means it's a duplicate entry
|
|
// in the same transaction
|
|
if _, ok := added[row.ID]; ok {
|
|
return rollback(&ErrOldVersion{})
|
|
}
|
|
added[row.ID] = true
|
|
}
|
|
return tx.Commit().Error
|
|
}
|
|
|
|
// GetCurrent gets a specific TUF record
|
|
func (db *SQLStorage) GetCurrent(gun, tufRole string) (*time.Time, []byte, error) {
|
|
var row TUFFile
|
|
q := db.Select("updated_at, data").Where(
|
|
&TUFFile{Gun: gun, Role: tufRole}).Order("version desc").Limit(1).First(&row)
|
|
if err := isReadErr(q, row); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return &(row.UpdatedAt), row.Data, nil
|
|
}
|
|
|
|
// GetChecksum gets a specific TUF record by its hex checksum
|
|
func (db *SQLStorage) GetChecksum(gun, tufRole, checksum string) (*time.Time, []byte, error) {
|
|
var row TUFFile
|
|
q := db.Select("created_at, data").Where(
|
|
&TUFFile{
|
|
Gun: gun,
|
|
Role: tufRole,
|
|
Sha256: checksum,
|
|
},
|
|
).First(&row)
|
|
if err := isReadErr(q, row); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return &(row.CreatedAt), row.Data, nil
|
|
}
|
|
|
|
func isReadErr(q *gorm.DB, row TUFFile) error {
|
|
if q.RecordNotFound() {
|
|
return ErrNotFound{}
|
|
} else if q.Error != nil {
|
|
return q.Error
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Delete deletes all the records for a specific GUN
|
|
func (db *SQLStorage) Delete(gun string) error {
|
|
return db.Where(&TUFFile{Gun: gun}).Delete(TUFFile{}).Error
|
|
}
|
|
|
|
// GetKey returns the Public Key data for a gun+role
|
|
func (db *SQLStorage) GetKey(gun, role string) (algorithm string, public []byte, err error) {
|
|
logrus.Debugf("retrieving timestamp key for %s:%s", gun, role)
|
|
|
|
var row Key
|
|
query := db.Select("cipher, public").Where(&Key{Gun: gun, Role: role}).Find(&row)
|
|
|
|
if query.RecordNotFound() {
|
|
return "", nil, &ErrNoKey{gun: gun}
|
|
} else if query.Error != nil {
|
|
return "", nil, query.Error
|
|
}
|
|
|
|
return row.Cipher, row.Public, nil
|
|
}
|
|
|
|
// SetKey attempts to write a key and returns an error if it already exists for the gun and role
|
|
func (db *SQLStorage) SetKey(gun, role, algorithm string, public []byte) error {
|
|
|
|
entry := Key{
|
|
Gun: gun,
|
|
Role: role,
|
|
}
|
|
|
|
if !db.Where(&entry).First(&Key{}).RecordNotFound() {
|
|
return &ErrKeyExists{gun: gun, role: role}
|
|
}
|
|
|
|
entry.Cipher = algorithm
|
|
entry.Public = public
|
|
|
|
return translateOldVersionError(
|
|
db.FirstOrCreate(&Key{}, &entry).Error)
|
|
}
|
|
|
|
// CheckHealth asserts that both required tables are present
|
|
func (db *SQLStorage) CheckHealth() error {
|
|
interfaces := []interface {
|
|
TableName() string
|
|
}{&TUFFile{}, &Key{}}
|
|
|
|
for _, model := range interfaces {
|
|
tableOk := db.HasTable(model)
|
|
if db.Error != nil {
|
|
return db.Error
|
|
}
|
|
if !tableOk {
|
|
return fmt.Errorf(
|
|
"Cannot access table: %s", model.TableName())
|
|
}
|
|
}
|
|
return nil
|
|
}
|