diff --git a/db/bolt/BoltDb.go b/db/bolt/BoltDb.go index 1e2f1093e..669835bf7 100644 --- a/db/bolt/BoltDb.go +++ b/db/bolt/BoltDb.go @@ -1,24 +1,29 @@ package bolt import ( + "encoding/json" "fmt" + "github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/util" "go.etcd.io/bbolt" + "reflect" + "sort" + "strconv" ) type BoltDb struct { db *bbolt.DB } -func makeObjectId(tableName string, ids ...int) ([]byte, error) { +func makeBucketId(obj db.ObjectProperties, ids ...int) []byte { n := len(ids) - id := tableName + id := obj.TableName for i := 0; i < n; i++ { id += fmt.Sprintf("_%010d", ids[i]) } - return []byte(id), nil + return []byte(id) } func (d *BoltDb) Migrate() error { @@ -30,14 +35,158 @@ func (d *BoltDb) Connect() error { if err != nil { return err } - db, err := bbolt.Open(config.Hostname, 0666, nil) + d.db, err = bbolt.Open(config.Hostname, 0666, nil) if err != nil { return err } - d.db = db return nil } func (d *BoltDb) Close() error { return d.db.Close() } + +func (d *BoltDb) getObject(projectID int, props db.ObjectProperties, objectID int, object interface{}) (err error) { + err = d.db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(makeBucketId(props, projectID)) + if b == nil { + return db.ErrNotFound + } + + id := []byte(strconv.Itoa(objectID)) + str := b.Get(id) + if str == nil { + return db.ErrNotFound + } + + return json.Unmarshal(str, &object) + }) + + return +} + +func getFieldNameByTag(t reflect.Type, tag string, value string) (string, error) { + n := t.NumField() + for i := 0; i < n; i++ { + if t.Field(i).Tag.Get(tag) == value { + return t.Field(i).Name, nil + } + } + return "", fmt.Errorf("") +} + +func sortObjects(objects interface{}, sortBy string, sortInverted bool) error { + objectsValue := reflect.ValueOf(objects).Elem() + objType := objectsValue.Type().Elem() + fieldName, err := getFieldNameByTag(objType, "db", sortBy) + if err != nil { + return err + } + + sort.SliceStable(objectsValue.Interface(), func (i, j int) bool { + fieldI := objectsValue.Index(i).FieldByName(fieldName) + fieldJ := objectsValue.Index(j).FieldByName(fieldName) + switch fieldJ.Kind() { + case reflect.Int: + case reflect.Int8: + case reflect.Int16: + case reflect.Int32: + case reflect.Int64: + case reflect.Uint: + case reflect.Uint8: + case reflect.Uint16: + case reflect.Uint32: + case reflect.Uint64: + return fieldI.Int() < fieldJ.Int() + case reflect.Float32: + case reflect.Float64: + return fieldI.Float() < fieldJ.Float() + case reflect.String: + return fieldI.String() < fieldJ.String() + } + return false + }) + + return nil +} + +func (d *BoltDb) getObjects(projectID int, props db.ObjectProperties, params db.RetrieveQueryParams, objects interface{}) (err error) { + objectsValue := reflect.ValueOf(objects).Elem() + objType := objectsValue.Type().Elem() + + // Read elements from database + err = d.db.View(func(tx *bbolt.Tx) error { + + b := tx.Bucket(makeBucketId(props, projectID)) + c := b.Cursor() + i := 0 // current item index + n := 0 // number of added items + + for k, v := c.First(); k != nil; k, v = c.Next() { + if i < params.Offset { + continue + } + + obj := reflect.New(objType).Elem() + err2 := json.Unmarshal(v, &obj) + if err2 == nil { + return err2 + } + + objectsValue.Set(reflect.Append(objectsValue, obj)) + + n++ + + if n > params.Count { + break + } + } + + return nil + }) + + if err != nil { + return + } + + + // Sort elements + err = sortObjects(objects, params.SortBy, params.SortInverted) + + return +} + + +func (d *BoltDb) isObjectInUse(projectID int, props db.ObjectProperties, objectID int) (inUse bool, err error) { + err = d.db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(makeBucketId(props, projectID)) + inUse = b != nil && b.Get([]byte(strconv.Itoa(objectID))) != nil + return nil + }) + + return +} + +func (d *BoltDb) deleteObject(projectID int, props db.ObjectProperties, objectID int) error { + inUse, err := d.isObjectInUse(projectID, props, objectID) + + if err != nil { + return err + } + + if inUse { + return db.ErrInvalidOperation + } + + return d.db.Update(func (tx *bbolt.Tx) error { + b := tx.Bucket(makeBucketId(db.InventoryObject, projectID)) + if b == nil { + return db.ErrNotFound + } + return b.Delete([]byte(strconv.Itoa(objectID))) + }) +} + +func (d *BoltDb) deleteObjectSoft(projectID int, props db.ObjectProperties, objectID int) error { + return d.deleteObject(projectID, props, objectID) +} diff --git a/db/bolt/BoltDb_test.go b/db/bolt/BoltDb_test.go new file mode 100644 index 000000000..4fcd8f9e8 --- /dev/null +++ b/db/bolt/BoltDb_test.go @@ -0,0 +1,48 @@ +package bolt + +import ( + "fmt" + "github.com/ansible-semaphore/semaphore/db" + "testing" +) + +func TestApiPing(t *testing.T) { + objects := []db.Inventory{ + { + ID: 1, + Name: "x", + }, + { + ID: 2, + Name: "a", + }, + { + ID: 3, + Name: "d", + }, + { + ID: 4, + Name: "b", + }, + { + ID: 5, + Name: "r", + }, + } + + err := sortObjects(&objects, "name", false) + if err != nil { + t.Fatal(err) + } + + expected := objects[0].Name == "a" && + objects[1].Name == "b" && + objects[2].Name == "d" && + objects[3].Name == "r" && + objects[4].Name == "x" + + + if !expected { + t.Fatal(fmt.Errorf("objects not sorted")) + } +} diff --git a/db/bolt/access_key.go b/db/bolt/access_key.go new file mode 100644 index 000000000..06900bae8 --- /dev/null +++ b/db/bolt/access_key.go @@ -0,0 +1,60 @@ +package bolt + +import "github.com/ansible-semaphore/semaphore/db" + +func (d *BoltDb) GetAccessKey(projectID int, accessKeyID int) (db.AccessKey, error) { + var key db.AccessKey + err := d.getObject(projectID, db.AccessKeyObject, accessKeyID, &key) + return key, err +} + +func (d *BoltDb) GetAccessKeys(projectID int, params db.RetrieveQueryParams) ([]db.AccessKey, error) { + var keys []db.AccessKey + err := d.getObjects(projectID, db.AccessKeyObject, params, &keys) + return keys, err +} + +func (d *BoltDb) UpdateAccessKey(key db.AccessKey) error { + return nil +} + +func (d *BoltDb) CreateAccessKey(key db.AccessKey) (newKey db.AccessKey, err error) { + return +} + +func (d *BoltDb) DeleteAccessKey(projectID int, accessKeyID int) error { + return d.deleteObject(projectID, db.AccessKeyObject, accessKeyID) +} + +func (d *BoltDb) DeleteAccessKeySoft(projectID int, accessKeyID int) error { + return d.deleteObjectSoft(projectID, db.AccessKeyObject, accessKeyID) +} + + +func (d *BoltDb) GetGlobalAccessKey(accessKeyID int) (db.AccessKey, error) { + var key db.AccessKey + err := d.getObject(0, db.GlobalAccessKeyObject, accessKeyID, &key) + return key, err +} + +func (d *BoltDb) GetGlobalAccessKeys(params db.RetrieveQueryParams) ([]db.AccessKey, error) { + var keys []db.AccessKey + err := d.getObjects(0, db.GlobalAccessKeyObject, params, &keys) + return keys, err +} + +func (d *BoltDb) UpdateGlobalAccessKey(key db.AccessKey) error { + return nil +} + +func (d *BoltDb) CreateGlobalAccessKey(key db.AccessKey) (newKey db.AccessKey, err error) { + return +} + +func (d *BoltDb) DeleteGlobalAccessKey(accessKeyID int) error { + return d.deleteObject(0, db.GlobalAccessKeyObject, accessKeyID) +} + +func (d *BoltDb) DeleteGlobalAccessKeySoft(accessKeyID int) error { + return d.deleteObjectSoft(0, db.GlobalAccessKeyObject, accessKeyID) +} diff --git a/db/bolt/inventory.go b/db/bolt/inventory.go index 315fe274f..742780224 100644 --- a/db/bolt/inventory.go +++ b/db/bolt/inventory.go @@ -3,16 +3,14 @@ package bolt import ( "encoding/json" "github.com/ansible-semaphore/semaphore/db" - bolt "go.etcd.io/bbolt" + "go.etcd.io/bbolt" "strconv" ) func (d *BoltDb) GetInventory(projectID int, inventoryID int) (inventory db.Inventory, err error) { - id, err := makeObjectId("inventory", projectID) - - err = d.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket(id) + err = d.db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(makeBucketId(db.InventoryObject, projectID)) if b == nil { return db.ErrNotFound } @@ -30,23 +28,23 @@ func (d *BoltDb) GetInventory(projectID int, inventoryID int) (inventory db.Inve return } - //if inventory.KeyID != nil { - // inventory.Key, err = d.GetAccessKey(projectID, *inventory.KeyID) - // if err != nil { - // return - // } - //} - // - //if inventory.SSHKeyID != nil { - // inventory.SSHKey, err = d.GetAccessKey(projectID, *inventory.SSHKeyID) - //} + if inventory.KeyID != nil { + inventory.Key, err = d.GetAccessKey(projectID, *inventory.KeyID) + if err != nil { + return + } + } + + if inventory.SSHKeyID != nil { + inventory.SSHKey, err = d.GetAccessKey(projectID, *inventory.SSHKeyID) + } return } func (d *BoltDb) GetInventories(projectID int, params db.RetrieveQueryParams) (inventories []db.Inventory, err error) { - err = d.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("inventory_" + strconv.Itoa(projectID))) + err = d.db.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(makeBucketId(db.InventoryObject, projectID)) if b == nil { return db.ErrNotFound } @@ -58,13 +56,14 @@ func (d *BoltDb) GetInventories(projectID int, params db.RetrieveQueryParams) (i } func (d *BoltDb) DeleteInventory(projectID int, inventoryID int) error { - return d.db.Update(func (tx *bolt.Tx) error { - b := tx.Bucket([]byte("inventory_" + strconv.Itoa(projectID))) + return d.db.Update(func (tx *bbolt.Tx) error { + b := tx.Bucket(makeBucketId(db.InventoryObject, projectID)) if b == nil { return db.ErrNotFound } return b.Delete([]byte(strconv.Itoa(inventoryID))) }) + } func (d *BoltDb) DeleteInventorySoft(projectID int, inventoryID int) error { @@ -72,7 +71,7 @@ func (d *BoltDb) DeleteInventorySoft(projectID int, inventoryID int) error { } func (d *BoltDb) UpdateInventory(inventory db.Inventory) error { - err := d.db.Update(func(tx *bolt.Tx) error { + err := d.db.Update(func(tx *bbolt.Tx) error { b := tx.Bucket([]byte("inventory_" + strconv.Itoa(inventory.ProjectID))) if b == nil { return db.ErrNotFound @@ -95,8 +94,8 @@ func (d *BoltDb) UpdateInventory(inventory db.Inventory) error { } func (d *BoltDb) CreateInventory(inventory db.Inventory) (newInventory db.Inventory, err error) { - err = d.db.Update(func(tx *bolt.Tx) error { - b, err2 := tx.CreateBucketIfNotExists([]byte("inventory_" + strconv.Itoa(inventory.ProjectID))) + err = d.db.Update(func(tx *bbolt.Tx) error { + b, err2 := tx.CreateBucketIfNotExists(makeBucketId(db.InventoryObject, inventory.ProjectID)) if err2 != nil { return err2 }