685 lines
15 KiB
Go
685 lines
15 KiB
Go
package bolt
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
bolt "github.com/coreos/bbolt"
|
|
platform "github.com/influxdata/influxdb"
|
|
platformcontext "github.com/influxdata/influxdb/context"
|
|
"github.com/influxdata/influxdb/kit/tracing"
|
|
)
|
|
|
|
var (
|
|
bucketBucket = []byte("bucketsv1")
|
|
bucketIndex = []byte("bucketindexv1")
|
|
)
|
|
|
|
var _ platform.BucketService = (*Client)(nil)
|
|
var _ platform.BucketOperationLogService = (*Client)(nil)
|
|
|
|
func (c *Client) initializeBuckets(ctx context.Context, tx *bolt.Tx) error {
|
|
if _, err := tx.CreateBucketIfNotExists([]byte(bucketBucket)); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.CreateBucketIfNotExists([]byte(bucketIndex)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FindBucketByID retrieves a bucket by id.
|
|
func (c *Client) FindBucketByID(ctx context.Context, id platform.ID) (*platform.Bucket, error) {
|
|
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
defer span.Finish()
|
|
|
|
var b *platform.Bucket
|
|
var err error
|
|
|
|
err = c.db.View(func(tx *bolt.Tx) error {
|
|
bkt, pe := c.findBucketByID(ctx, tx, id)
|
|
if pe != nil {
|
|
pe.Op = getOp(platform.OpFindBucketByID)
|
|
err = pe
|
|
return err
|
|
}
|
|
b = bkt
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (c *Client) findBucketByID(ctx context.Context, tx *bolt.Tx, id platform.ID) (*platform.Bucket, *platform.Error) {
|
|
span, _ := tracing.StartSpanFromContext(ctx)
|
|
defer span.Finish()
|
|
|
|
var b platform.Bucket
|
|
|
|
encodedID, err := id.Encode()
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Code: platform.EInvalid,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
v := tx.Bucket(bucketBucket).Get(encodedID)
|
|
if len(v) == 0 {
|
|
return nil, &platform.Error{
|
|
Code: platform.ENotFound,
|
|
Msg: "bucket not found",
|
|
}
|
|
}
|
|
|
|
if err := json.Unmarshal(v, &b); err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
return &b, nil
|
|
}
|
|
|
|
// FindBucketByName returns a bucket by name for a particular organization.
|
|
// TODO: have method for finding bucket using organization name and bucket name.
|
|
func (c *Client) FindBucketByName(ctx context.Context, orgID platform.ID, n string) (*platform.Bucket, error) {
|
|
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
defer span.Finish()
|
|
|
|
var b *platform.Bucket
|
|
var err error
|
|
|
|
err = c.db.View(func(tx *bolt.Tx) error {
|
|
bkt, pe := c.findBucketByName(ctx, tx, orgID, n)
|
|
if pe != nil {
|
|
pe.Op = getOp(platform.OpFindBucket)
|
|
err = pe
|
|
return err
|
|
}
|
|
b = bkt
|
|
return nil
|
|
})
|
|
|
|
return b, err
|
|
}
|
|
|
|
func (c *Client) findBucketByName(ctx context.Context, tx *bolt.Tx, orgID platform.ID, n string) (*platform.Bucket, *platform.Error) {
|
|
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
defer span.Finish()
|
|
|
|
b := &platform.Bucket{
|
|
OrgID: orgID,
|
|
Name: n,
|
|
}
|
|
key, err := bucketIndexKey(b)
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Code: platform.EInvalid,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
buf := tx.Bucket(bucketIndex).Get(key)
|
|
if buf == nil {
|
|
return nil, &platform.Error{
|
|
Code: platform.ENotFound,
|
|
Msg: fmt.Sprintf("bucket %q not found", n),
|
|
}
|
|
}
|
|
|
|
var id platform.ID
|
|
if err := id.Decode(buf); err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
return c.findBucketByID(ctx, tx, id)
|
|
}
|
|
|
|
// FindBucket retrives a bucket using an arbitrary bucket filter.
|
|
// Filters using ID, or OrganizationID and bucket Name should be efficient.
|
|
// Other filters will do a linear scan across buckets until it finds a match.
|
|
func (c *Client) FindBucket(ctx context.Context, filter platform.BucketFilter) (*platform.Bucket, error) {
|
|
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
defer span.Finish()
|
|
|
|
var b *platform.Bucket
|
|
var err error
|
|
|
|
if filter.ID != nil {
|
|
b, err = c.FindBucketByID(ctx, *filter.ID)
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Op: getOp(platform.OpFindBucket),
|
|
Err: err,
|
|
}
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
if filter.Name != nil && filter.OrganizationID != nil {
|
|
return c.FindBucketByName(ctx, *filter.OrganizationID, *filter.Name)
|
|
}
|
|
|
|
err = c.db.View(func(tx *bolt.Tx) error {
|
|
if filter.Org != nil {
|
|
o, err := c.findOrganizationByName(ctx, tx, *filter.Org)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filter.OrganizationID = &o.ID
|
|
}
|
|
|
|
filterFn := filterBucketsFn(filter)
|
|
return c.forEachBucket(ctx, tx, false, func(bkt *platform.Bucket) bool {
|
|
if filterFn(bkt) {
|
|
b = bkt
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Op: getOp(platform.OpFindBucket),
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
if b == nil {
|
|
return nil, &platform.Error{
|
|
Code: platform.ENotFound,
|
|
Msg: "bucket not found",
|
|
}
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func filterBucketsFn(filter platform.BucketFilter) func(b *platform.Bucket) bool {
|
|
if filter.ID != nil {
|
|
return func(b *platform.Bucket) bool {
|
|
return b.ID == *filter.ID
|
|
}
|
|
}
|
|
|
|
if filter.Name != nil && filter.OrganizationID != nil {
|
|
return func(b *platform.Bucket) bool {
|
|
return b.Name == *filter.Name && b.OrgID == *filter.OrganizationID
|
|
}
|
|
}
|
|
|
|
if filter.Name != nil {
|
|
return func(b *platform.Bucket) bool {
|
|
return b.Name == *filter.Name
|
|
}
|
|
}
|
|
|
|
if filter.OrganizationID != nil {
|
|
return func(b *platform.Bucket) bool {
|
|
return b.OrgID == *filter.OrganizationID
|
|
}
|
|
}
|
|
|
|
return func(b *platform.Bucket) bool { return true }
|
|
}
|
|
|
|
// FindBuckets retrives all buckets that match an arbitrary bucket filter.
|
|
// Filters using ID, or OrganizationID and bucket Name should be efficient.
|
|
// Other filters will do a linear scan across all buckets searching for a match.
|
|
func (c *Client) FindBuckets(ctx context.Context, filter platform.BucketFilter, opts ...platform.FindOptions) ([]*platform.Bucket, int, error) {
|
|
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
defer span.Finish()
|
|
|
|
if filter.ID != nil {
|
|
b, err := c.FindBucketByID(ctx, *filter.ID)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return []*platform.Bucket{b}, 1, nil
|
|
}
|
|
|
|
if filter.Name != nil && filter.OrganizationID != nil {
|
|
b, err := c.FindBucketByName(ctx, *filter.OrganizationID, *filter.Name)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return []*platform.Bucket{b}, 1, nil
|
|
}
|
|
|
|
bs := []*platform.Bucket{}
|
|
err := c.db.View(func(tx *bolt.Tx) error {
|
|
bkts, err := c.findBuckets(ctx, tx, filter, opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bs = bkts
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return bs, len(bs), nil
|
|
}
|
|
|
|
func (c *Client) findBuckets(ctx context.Context, tx *bolt.Tx, filter platform.BucketFilter, opts ...platform.FindOptions) ([]*platform.Bucket, *platform.Error) {
|
|
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
defer span.Finish()
|
|
|
|
bs := []*platform.Bucket{}
|
|
if filter.Org != nil {
|
|
o, err := c.findOrganizationByName(ctx, tx, *filter.Org)
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
filter.OrganizationID = &o.ID
|
|
}
|
|
|
|
var offset, limit, count int
|
|
var descending bool
|
|
if len(opts) > 0 {
|
|
offset = opts[0].Offset
|
|
limit = opts[0].Limit
|
|
descending = opts[0].Descending
|
|
}
|
|
|
|
filterFn := filterBucketsFn(filter)
|
|
err := c.forEachBucket(ctx, tx, descending, func(b *platform.Bucket) bool {
|
|
if filterFn(b) {
|
|
if count >= offset {
|
|
bs = append(bs, b)
|
|
}
|
|
count++
|
|
}
|
|
|
|
if limit > 0 && len(bs) >= limit {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
return bs, nil
|
|
}
|
|
|
|
// CreateBucket creates a platform bucket and sets b.ID.
|
|
func (c *Client) CreateBucket(ctx context.Context, b *platform.Bucket) error {
|
|
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
defer span.Finish()
|
|
|
|
var err error
|
|
op := getOp(platform.OpCreateBucket)
|
|
return c.db.Update(func(tx *bolt.Tx) error {
|
|
if b.OrgID.Valid() {
|
|
_, pe := c.findOrganizationByID(ctx, tx, b.OrgID)
|
|
if pe != nil {
|
|
return &platform.Error{
|
|
Err: pe,
|
|
Op: op,
|
|
}
|
|
}
|
|
}
|
|
|
|
unique := c.uniqueBucketName(ctx, tx, b)
|
|
|
|
if !unique {
|
|
// TODO: make standard error
|
|
return &platform.Error{
|
|
Code: platform.EConflict,
|
|
Op: op,
|
|
Msg: fmt.Sprintf("bucket with name %s already exists", b.Name),
|
|
}
|
|
}
|
|
|
|
b.ID = c.IDGenerator.ID()
|
|
b.CreatedAt = c.Now()
|
|
b.UpdatedAt = c.Now()
|
|
|
|
if err = c.appendBucketEventToLog(ctx, tx, b.ID, bucketCreatedEvent); err != nil {
|
|
return &platform.Error{
|
|
Op: op,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
if pe := c.putBucket(ctx, tx, b); pe != nil {
|
|
pe.Op = op
|
|
err = pe
|
|
}
|
|
|
|
if pe := c.createBucketUserResourceMappings(ctx, tx, b); pe != nil {
|
|
pe.Op = op
|
|
err = pe
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// PutBucket will put a bucket without setting an ID.
|
|
func (c *Client) PutBucket(ctx context.Context, b *platform.Bucket) error {
|
|
return c.db.Update(func(tx *bolt.Tx) error {
|
|
var err error
|
|
pe := c.putBucket(ctx, tx, b)
|
|
if pe != nil {
|
|
err = pe
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
func (c *Client) createBucketUserResourceMappings(ctx context.Context, tx *bolt.Tx, b *platform.Bucket) *platform.Error {
|
|
ms, err := c.findUserResourceMappings(ctx, tx, platform.UserResourceMappingFilter{
|
|
ResourceType: platform.OrgsResourceType,
|
|
ResourceID: b.OrgID,
|
|
})
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
for _, m := range ms {
|
|
if err := c.createUserResourceMapping(ctx, tx, &platform.UserResourceMapping{
|
|
ResourceType: platform.BucketsResourceType,
|
|
ResourceID: b.ID,
|
|
UserID: m.UserID,
|
|
UserType: m.UserType,
|
|
}); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) putBucket(ctx context.Context, tx *bolt.Tx, b *platform.Bucket) *platform.Error {
|
|
v, err := json.Marshal(b)
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
encodedID, err := b.ID.Encode()
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
key, pe := bucketIndexKey(b)
|
|
if err != nil {
|
|
return pe
|
|
}
|
|
|
|
if err := tx.Bucket(bucketIndex).Put(key, encodedID); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
if err := tx.Bucket(bucketBucket).Put(encodedID, v); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func bucketIndexKey(b *platform.Bucket) ([]byte, *platform.Error) {
|
|
orgID, err := b.OrgID.Encode()
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Code: platform.EInvalid,
|
|
Err: err,
|
|
}
|
|
}
|
|
k := make([]byte, platform.IDLength+len(b.Name))
|
|
copy(k, orgID)
|
|
copy(k[platform.IDLength:], []byte(b.Name))
|
|
return k, nil
|
|
}
|
|
|
|
// forEachBucket will iterate through all buckets while fn returns true.
|
|
func (c *Client) forEachBucket(ctx context.Context, tx *bolt.Tx, descending bool, fn func(*platform.Bucket) bool) error {
|
|
cur := tx.Bucket(bucketBucket).Cursor()
|
|
|
|
var k, v []byte
|
|
if descending {
|
|
k, v = cur.Last()
|
|
} else {
|
|
k, v = cur.First()
|
|
}
|
|
|
|
for k != nil {
|
|
b := &platform.Bucket{}
|
|
if err := json.Unmarshal(v, b); err != nil {
|
|
return err
|
|
}
|
|
if !fn(b) {
|
|
break
|
|
}
|
|
|
|
if descending {
|
|
k, v = cur.Prev()
|
|
} else {
|
|
k, v = cur.Next()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) uniqueBucketName(ctx context.Context, tx *bolt.Tx, b *platform.Bucket) bool {
|
|
key, err := bucketIndexKey(b)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
v := tx.Bucket(bucketIndex).Get(key)
|
|
return len(v) == 0
|
|
}
|
|
|
|
// UpdateBucket updates a bucket according the parameters set on upd.
|
|
func (c *Client) UpdateBucket(ctx context.Context, id platform.ID, upd platform.BucketUpdate) (*platform.Bucket, error) {
|
|
var b *platform.Bucket
|
|
err := c.db.Update(func(tx *bolt.Tx) error {
|
|
bkt, err := c.updateBucket(ctx, tx, id, upd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b = bkt
|
|
return nil
|
|
})
|
|
|
|
return b, err
|
|
}
|
|
|
|
func (c *Client) updateBucket(ctx context.Context, tx *bolt.Tx, id platform.ID, upd platform.BucketUpdate) (*platform.Bucket, error) {
|
|
b, err := c.findBucketByID(ctx, tx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if upd.RetentionPeriod != nil {
|
|
b.RetentionPeriod = *upd.RetentionPeriod
|
|
}
|
|
|
|
if upd.Description != nil {
|
|
b.Description = *upd.Description
|
|
}
|
|
|
|
if upd.Name != nil {
|
|
b0, err := c.findBucketByName(ctx, tx, b.OrgID, *upd.Name)
|
|
if err == nil && b0.ID != id {
|
|
return nil, &platform.Error{
|
|
Code: platform.EConflict,
|
|
Msg: "bucket name is not unique",
|
|
}
|
|
}
|
|
|
|
key, err := bucketIndexKey(b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Buckets are indexed by name and so the bucket index must be pruned when name is modified.
|
|
if err := tx.Bucket(bucketIndex).Delete(key); err != nil {
|
|
return nil, err
|
|
}
|
|
b.Name = *upd.Name
|
|
}
|
|
|
|
b.UpdatedAt = c.Now()
|
|
|
|
if err := c.appendBucketEventToLog(ctx, tx, b.ID, bucketUpdatedEvent); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := c.putBucket(ctx, tx, b); err != nil {
|
|
return nil, err
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// DeleteBucket deletes a bucket and prunes it from the index.
|
|
func (c *Client) DeleteBucket(ctx context.Context, id platform.ID) error {
|
|
return c.db.Update(func(tx *bolt.Tx) error {
|
|
var err error
|
|
if pe := c.deleteBucket(ctx, tx, id); pe != nil {
|
|
pe.Op = getOp(platform.OpDeleteBucket)
|
|
err = pe
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
func (c *Client) deleteBucket(ctx context.Context, tx *bolt.Tx, id platform.ID) *platform.Error {
|
|
b, pe := c.findBucketByID(ctx, tx, id)
|
|
if pe != nil {
|
|
return pe
|
|
}
|
|
key, pe := bucketIndexKey(b)
|
|
if pe != nil {
|
|
return pe
|
|
}
|
|
// make lowercase deleteBucket with tx
|
|
if err := tx.Bucket(bucketIndex).Delete(key); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
encodedID, err := id.Encode()
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Code: platform.EInvalid,
|
|
Err: err,
|
|
}
|
|
}
|
|
if err := tx.Bucket(bucketBucket).Delete(encodedID); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
if err := c.deleteUserResourceMappings(ctx, tx, platform.UserResourceMappingFilter{
|
|
ResourceID: id,
|
|
ResourceType: platform.BucketsResourceType,
|
|
}); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const bucketOperationLogKeyPrefix = "bucket"
|
|
|
|
func encodeBucketOperationLogKey(id platform.ID) ([]byte, error) {
|
|
buf, err := id.Encode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append([]byte(bucketOperationLogKeyPrefix), buf...), nil
|
|
}
|
|
|
|
// GetBucketOperationLog retrieves a buckets operation log.
|
|
func (c *Client) GetBucketOperationLog(ctx context.Context, id platform.ID, opts platform.FindOptions) ([]*platform.OperationLogEntry, int, error) {
|
|
// TODO(desa): might be worthwhile to allocate a slice of size opts.Limit
|
|
log := []*platform.OperationLogEntry{}
|
|
|
|
err := c.db.View(func(tx *bolt.Tx) error {
|
|
key, err := encodeBucketOperationLogKey(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.forEachLogEntry(ctx, tx, key, opts, func(v []byte, t time.Time) error {
|
|
e := &platform.OperationLogEntry{}
|
|
if err := json.Unmarshal(v, e); err != nil {
|
|
return err
|
|
}
|
|
e.Time = t
|
|
|
|
log = append(log, e)
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return log, len(log), nil
|
|
}
|
|
|
|
// TODO(desa): what do we want these to be?
|
|
const (
|
|
bucketCreatedEvent = "Bucket Created"
|
|
bucketUpdatedEvent = "Bucket Updated"
|
|
)
|
|
|
|
func (c *Client) appendBucketEventToLog(ctx context.Context, tx *bolt.Tx, id platform.ID, s string) error {
|
|
e := &platform.OperationLogEntry{
|
|
Description: s,
|
|
}
|
|
// TODO(desa): this is fragile and non explicit since it requires an authorizer to be on context. It should be
|
|
// replaced with a higher level transaction so that adding to the log can take place in the http handler
|
|
// where the userID will exist explicitly.
|
|
a, err := platformcontext.GetAuthorizer(ctx)
|
|
if err == nil {
|
|
// Add the user to the log if you can, but don't error if its not there.
|
|
e.UserID = a.GetUserID()
|
|
}
|
|
|
|
v, err := json.Marshal(e)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
k, err := encodeBucketOperationLogKey(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.addLogEntry(ctx, tx, k, v, c.Now())
|
|
}
|