mirror of
https://github.com/clearlinux/rkt.git
synced 2026-08-18 21:16:13 +00:00
9685f37575
Inspired by a similar change in the Kubernetes project [1], this changes the copyright header to the more generic "The rkt Authors" rather than "CoreOS, Inc.", which is only sometimes correct. https://github.com/GoogleCloudPlatform/kubernetes/commit/6b3a6e6b983f967c88d14d26542ec6e30c49ebd3
397 lines
9.0 KiB
Go
397 lines
9.0 KiB
Go
// Copyright 2015 The rkt 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 store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type testdb interface {
|
|
version() int
|
|
populate(db *DB) error
|
|
load(db *DB) error
|
|
compare(db testdb) bool
|
|
}
|
|
|
|
type DBV0 struct {
|
|
aciinfos []*ACIInfoV0_1
|
|
remotes []*RemoteV0_1
|
|
}
|
|
|
|
func (d *DBV0) version() int {
|
|
return 0
|
|
}
|
|
|
|
func (d *DBV0) populate(db *DB) error {
|
|
// As DBV0 and DBV1 have the same schema use a common populate
|
|
// function.
|
|
return populateDBV0_1(db, d.version(), d.aciinfos, d.remotes)
|
|
}
|
|
|
|
// load populates the given struct with the data in db.
|
|
// the given struct d should be empty
|
|
func (d *DBV0) load(db *DB) error {
|
|
fn := func(tx *sql.Tx) error {
|
|
var err error
|
|
d.aciinfos, err = getAllACIInfosV0_1(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.remotes, err = getAllRemoteV0_1(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
if err := db.Do(fn); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *DBV0) compare(td testdb) bool {
|
|
d2, ok := td.(*DBV0)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if !compareSlicesNoOrder(d.aciinfos, d2.aciinfos) {
|
|
return false
|
|
}
|
|
if !compareSlicesNoOrder(d.remotes, d2.remotes) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
type DBV1 struct {
|
|
aciinfos []*ACIInfoV0_1
|
|
remotes []*RemoteV0_1
|
|
}
|
|
|
|
func (d *DBV1) version() int {
|
|
return 1
|
|
}
|
|
func (d *DBV1) populate(db *DB) error {
|
|
return populateDBV0_1(db, d.version(), d.aciinfos, d.remotes)
|
|
}
|
|
|
|
func (d *DBV1) load(db *DB) error {
|
|
fn := func(tx *sql.Tx) error {
|
|
var err error
|
|
d.aciinfos, err = getAllACIInfosV0_1(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.remotes, err = getAllRemoteV0_1(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
if err := db.Do(fn); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *DBV1) compare(td testdb) bool {
|
|
d2, ok := td.(*DBV1)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if !compareSlicesNoOrder(d.aciinfos, d2.aciinfos) {
|
|
return false
|
|
}
|
|
if !compareSlicesNoOrder(d.remotes, d2.remotes) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// The ACIInfo struct for different db versions. The ending VX_Y represent the
|
|
// first and the last version where the format isn't changed
|
|
// The latest existing struct should to be updated when updating the db version
|
|
// without changing the struct format (ex. V0_1 to V0_2).
|
|
// A new struct and its relative function should be added if the format is changed.
|
|
// The same applies for all of the the other structs.
|
|
type ACIInfoV0_1 struct {
|
|
BlobKey string
|
|
AppName string
|
|
ImportTime time.Time
|
|
Latest bool
|
|
}
|
|
|
|
func getAllACIInfosV0_1(tx *sql.Tx) ([]*ACIInfoV0_1, error) {
|
|
aciinfos := []*ACIInfoV0_1{}
|
|
rows, err := tx.Query("SELECT * from aciinfo")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
aciinfo := &ACIInfoV0_1{}
|
|
if err := rows.Scan(&aciinfo.BlobKey, &aciinfo.AppName, &aciinfo.ImportTime, &aciinfo.Latest); err != nil {
|
|
return nil, err
|
|
}
|
|
aciinfos = append(aciinfos, aciinfo)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return aciinfos, nil
|
|
}
|
|
|
|
type RemoteV0_1 struct {
|
|
ACIURL string
|
|
SigURL string
|
|
ETag string
|
|
BlobKey string
|
|
}
|
|
|
|
func getAllRemoteV0_1(tx *sql.Tx) ([]*RemoteV0_1, error) {
|
|
remotes := []*RemoteV0_1{}
|
|
rows, err := tx.Query("SELECT * from remote")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
remote := &RemoteV0_1{}
|
|
if err := rows.Scan(&remote.ACIURL, &remote.SigURL, &remote.ETag, &remote.BlobKey); err != nil {
|
|
return nil, err
|
|
}
|
|
remotes = append(remotes, remote)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return remotes, nil
|
|
}
|
|
|
|
func populateDBV0_1(db *DB, dbVersion int, aciInfos []*ACIInfoV0_1, remotes []*RemoteV0_1) error {
|
|
var dbCreateStmts = [...]string{
|
|
// version table
|
|
"CREATE TABLE IF NOT EXISTS version (version int);",
|
|
fmt.Sprintf("INSERT INTO version VALUES (%d)", dbVersion),
|
|
|
|
// remote table. The primary key is "aciurl".
|
|
"CREATE TABLE IF NOT EXISTS remote (aciurl string, sigurl string, etag string, blobkey string);",
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS aciurlidx ON remote (aciurl)",
|
|
|
|
// aciinfo table. The primary key is "blobkey" and it matches the key used to save that aci in the blob store
|
|
"CREATE TABLE IF NOT EXISTS aciinfo (blobkey string, appname string, importtime time, latest bool);",
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS blobkeyidx ON aciinfo (blobkey)",
|
|
"CREATE INDEX IF NOT EXISTS appnameidx ON aciinfo (appname)",
|
|
}
|
|
|
|
fn := func(tx *sql.Tx) error {
|
|
for _, stmt := range dbCreateStmts {
|
|
_, err := tx.Exec(stmt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := db.Do(fn); err != nil {
|
|
return err
|
|
}
|
|
|
|
fn = func(tx *sql.Tx) error {
|
|
for _, aciinfo := range aciInfos {
|
|
_, err := tx.Exec("INSERT into aciinfo values ($1, $2, $3, $4)", aciinfo.BlobKey, aciinfo.AppName, aciinfo.ImportTime, aciinfo.Latest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := db.Do(fn); err != nil {
|
|
return err
|
|
}
|
|
|
|
fn = func(tx *sql.Tx) error {
|
|
for _, remote := range remotes {
|
|
_, err := tx.Exec("INSERT into remote values ($1, $2, $3, $4)", remote.ACIURL, remote.SigURL, remote.ETag, remote.BlobKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := db.Do(fn); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type migrateTest struct {
|
|
predb testdb
|
|
postdb testdb
|
|
// Needed to have the right DB type to load from
|
|
curdb testdb
|
|
}
|
|
|
|
func testMigrate(tt migrateTest) error {
|
|
dir, err := ioutil.TempDir("", tstprefix)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating tempdir: %v", err)
|
|
}
|
|
defer os.RemoveAll(dir)
|
|
|
|
casDir := filepath.Join(dir, "cas")
|
|
db, err := NewDB(filepath.Join(casDir, "db"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tt.predb.populate(db); err != nil {
|
|
return err
|
|
}
|
|
|
|
fn := func(tx *sql.Tx) error {
|
|
err := migrate(tx, tt.postdb.version())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
if err = db.Do(fn); err != nil {
|
|
return err
|
|
}
|
|
|
|
var curDBVersion int
|
|
fn = func(tx *sql.Tx) error {
|
|
var err error
|
|
curDBVersion, err = getDBVersion(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
if err = db.Do(fn); err != nil {
|
|
return err
|
|
}
|
|
if curDBVersion != tt.postdb.version() {
|
|
return fmt.Errorf("wrong db version: got %#v, want %#v", curDBVersion, tt.postdb.version())
|
|
}
|
|
|
|
if err := tt.curdb.load(db); err != nil {
|
|
return err
|
|
}
|
|
if !tt.curdb.compare(tt.postdb) {
|
|
// TODO(sgotti) not very useful as these are pointers.
|
|
// Use something like go-spew to write the full data?
|
|
return fmt.Errorf("got %#v, want %#v", tt.curdb, tt.postdb)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestMigrate(t *testing.T) {
|
|
dir, err := ioutil.TempDir("", tstprefix)
|
|
if err != nil {
|
|
t.Fatalf("error creating tempdir: %v", err)
|
|
}
|
|
defer os.RemoveAll(dir)
|
|
|
|
now := time.Now()
|
|
tests := []migrateTest{
|
|
// Test migration from V0 to V1
|
|
// Empty db
|
|
{
|
|
&DBV0{},
|
|
&DBV1{},
|
|
&DBV1{},
|
|
},
|
|
{
|
|
&DBV0{
|
|
[]*ACIInfoV0_1{
|
|
{"sha512-aaaaaaaa", "example.com/app01", now, false},
|
|
{"sha512-bbbbbbbb", "example.com/app02", now, true},
|
|
},
|
|
[]*RemoteV0_1{
|
|
{"http://example.com/app01.aci", "http://example.com/app01.aci.asc", "", "sha512-aaaaaaaa"},
|
|
{"http://example.com/app02.aci", "http://example.com/app02.aci.asc", "", "sha512-bbbbbbbb"},
|
|
},
|
|
},
|
|
&DBV1{
|
|
[]*ACIInfoV0_1{
|
|
{"sha512-aaaaaaaa", "example.com/app01", now, false},
|
|
{"sha512-bbbbbbbb", "example.com/app02", now, true},
|
|
},
|
|
[]*RemoteV0_1{
|
|
{"http://example.com/app01.aci", "http://example.com/app01.aci.asc", "", "sha512-aaaaaaaa"},
|
|
{"http://example.com/app02.aci", "http://example.com/app02.aci.asc", "", "sha512-bbbbbbbb"},
|
|
},
|
|
},
|
|
&DBV1{},
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
if err := testMigrate(tt); err != nil {
|
|
t.Errorf("#%d: unexpected error: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// compareSlices compare slices regardless of the slice elements order
|
|
func compareSlicesNoOrder(i1 interface{}, i2 interface{}) bool {
|
|
s1 := interfaceToSlice(i1)
|
|
s2 := interfaceToSlice(i2)
|
|
|
|
if len(s1) != len(s2) {
|
|
return false
|
|
}
|
|
|
|
seen := map[int]bool{}
|
|
for _, v1 := range s1 {
|
|
found := false
|
|
for i2, v2 := range s2 {
|
|
if _, ok := seen[i2]; ok {
|
|
continue
|
|
}
|
|
if reflect.DeepEqual(v1, v2) {
|
|
found = true
|
|
seen[i2] = true
|
|
continue
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
|
|
}
|
|
return true
|
|
}
|
|
|
|
func interfaceToSlice(s interface{}) []interface{} {
|
|
v := reflect.ValueOf(s)
|
|
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
|
panic(fmt.Errorf("Expected slice or array, got %T", s))
|
|
}
|
|
l := v.Len()
|
|
m := make([]interface{}, l)
|
|
for i := 0; i < l; i++ {
|
|
m[i] = v.Index(i).Interface()
|
|
}
|
|
return m
|
|
}
|