feat(kv): implemented key/value store with end-to-end integration tests

* feat(kv:inmem:bolt): implement user service in a kv

* refactor(kv): use consistent func receiver name

* feat(kv): add initial basic auth service

* refactor(passwords): move auth interface into own file

* refactor(passwords): rename basic auth files to passwords

* refactor(passwords): rename from BasicAuth to Passwords

* refactor(kv): copy bolt user test into kv

Co-authored-by: Michael Desa <mjdesa@gmail.com>

* feat(kv): add inmem testing to kv store

* fix(kv): remove extra user index initialization

* feat(kv): attempt at making errors nice

* fix(http): return not found error if filter is invalid

* fix(http): s/platform/influxdb/ for user service

* fix(http): s/platform/influxdb/ for user service

* feat(kv): initial port of telegraf configs to kv

* feat(kv): first pass at migrating bolt org service to kv

* feat(kv): first pass at bucket service

* feat(kv): first pass at migrating kvlog to kv package

* feat(kv): add resource op logs

* feat(kv): first pass at user resource mapping migration

* feat(kv): add urm usage to bucket and org services

* feat(kv): first pass at kv authz service

* feat(kv): add cascading auth delete for users

* feat(kv): first pass d authorizer.OrganizationService in kv

* feat(cmd/influxd/launcher): user kv services where appropriate

* fix(kv): initialize authorizations

* fix(influxdb): use same buckets while slowly migrating stuff

* fix(kv): make staticcheck pass

* feat(kv): add dashboards to kv

review: make suggestions from pr review

fix: use common bucket names for bolt/kv stores

* test(kv): add complete password test coverage

* chore(kv): fixes for staticcheck

* feat(kv): implement labels generically on kv

* feat(kv): implement macro service

* feat(kv): add source service

* feat(kv): add session service

* feat(kv): add kv secret service

* refactor(kv): update telegraf and urm with error messages

* feat(kv): add lookup service

* feat(kv): add kv onboarding service

* refactor(kv): update telegraf to avoid repetition

* feat(cmd/influxd): use kv lookup service

* feat(kv): add telegraf to lookup service

* feat(cmd/influxd): use kv telegraf service

* feat(kv): initial port of scrapers in bolt to kv

* feat(kv): update scraper error messaging

* feat(cmd/influxd): add kv scraper

* feat(kv): add inmem backend tests

* refactor(kv): copy paste errors

* refactor(kv): add code to password errors

* fix(testing): update error messages for incorrect passwords

* feat(kv:inmem:bolt): implement user service in a kv

* refactor(kv): use consistent func receiver name

* refactor(kv): copy bolt user test into kv

Co-authored-by: Michael Desa <mjdesa@gmail.com>

* feat(kv): add inmem testing to kv store

* fix(kv): remove extra user index initialization

* feat(kv): attempt at making errors nice

* fix(http): return not found error if filter is invalid

* fix(http): s/platform/influxdb/ for user service

* feat(kv): first pass at migrating bolt org service to kv

* feat(kv): first pass at bucket service

* feat(kv): first pass at migrating kvlog to kv package

* feat(kv): add resource op logs

* feat(kv): first pass at user resource mapping migration

* feat(kv): add urm usage to bucket and org services

* feat(kv): first pass at kv authz service

* feat(kv): add cascading auth delete for users

* feat(kv): first pass d authorizer.OrganizationService in kv

* feat(cmd/influxd/launcher): user kv services where appropriate

* feat(kv): add initial basic auth service

* refactor(passwords): move auth interface into own file

* refactor(passwords): rename basic auth files to passwords

* fix(http): s/platform/influxdb/ for user service

* fix(kv): initialize authorizations

* fix(influxdb): use same buckets while slowly migrating stuff

* fix(kv): make staticcheck pass

* feat(kv): add dashboards to kv

review: make suggestions from pr review

fix: use common bucket names for bolt/kv stores

* feat(kv): implement labels generically on kv

* refactor(passwords): rename from BasicAuth to Passwords

* test(kv): add complete password test coverage

* chore(kv): fixes for staticcheck

* feat(kv): implement macro service

* feat(kv): add source service

* feat(kv): add session service

* feat(kv): initial port of telegraf configs to kv

* feat(kv): initial port of scrapers in bolt to kv

* feat(kv): add kv secret service

* refactor(kv): update telegraf and urm with error messages

* feat(kv): add lookup service

* feat(kv): add kv onboarding service

* refactor(kv): update telegraf to avoid repetition

* feat(cmd/influxd): use kv lookup service

* feat(kv): add telegraf to lookup service

* feat(cmd/influxd): use kv telegraf service

* feat(kv): update scraper error messaging

* feat(cmd/influxd): add kv scraper

* feat(kv): add inmem backend tests

* refactor(kv): copy paste errors

* refactor(kv): add code to password errors

* fix(testing): update error messages for incorrect passwords

* feat(http): initial support for flushing all key/values from kv store

* feat(kv): rename macro to variable

* feat(cmd/influxd/launcher): user kv services where appropriate

* refactor(passwords): rename from BasicAuth to Passwords

* feat(kv): implement macro service

* test(ui): introduce cypress

* test(ui): introduce first typescript test

* test(ui/e2e): add ci job

* chore: update gitignore to ignore test outputs

* feat(inmem): in memory influxdb

* test(e2e): adding pinger that checks if influxdb is alive

* hackathon

* hack

* hack

* hack

* hack

* Revert "feat(inmem): in memory influxdb"

This reverts commit 30ddf032003e704643b07ce80df61c3299ea7295.

* hack

* hack

* hack

* hack

* hack

* hack

* hack

* hack

* hack

* hack

* hack

* hack

* hack

* chore: lint ignore node_modules

* hack

* hack

* hack

* add user and flush

* hack

* remove unused vars

* hack

* hack

* ci(circle): prefix e2e artifacts

* change test to testid

* update cypress

* moar testid

* fix npm warnings

* remove absolte path

* chore(ci): remove /home/circleci proto mkdir hack

* wip: crud resources e2e

* fix(inmem): use inmem kv store services

* test(dashboard): add first dashboard crud tests

* hack

* undo hack

* fix: use response from setup for orgID

* chore: wip

* add convenience getByTitle function

* test(e2e): ui can create orgs

* test(e2e): add test for org deletion and update

* test(e2e): introduce task creation test

* test(e2e): create and update of buckets on org view

* chore: move types to declaration file

* chore: use route fixture in dashboard tests

* chore(ci): hack back

* test(ui): update snapshots

* chore: package-lock

* chore: remove macros

* fix: launcher rebase issues

* fix: compile errors

* fix: compile errors

* feat(cmd/influxdb): add explicit testing, asset-path, and store flags

Co-authored-by: Andrew Watkins <watts@influxdb.com>

* fix(cmd/influxd): set default HTTP handler and flags

Co-authored-by: Andrew Watkins <watts@influxdb.com>

* build(Makefile): add run-e2e and PHONY

* feat(kv:inmem:bolt): implement user service in a kv

* refactor(kv): use consistent func receiver name

* feat(kv): add initial basic auth service

* refactor(passwords): move auth interface into own file

* refactor(passwords): rename basic auth files to passwords

* refactor(passwords): rename from BasicAuth to Passwords

* refactor(kv): copy bolt user test into kv

Co-authored-by: Michael Desa <mjdesa@gmail.com>

* feat(kv): add inmem testing to kv store

* fix(kv): remove extra user index initialization

* feat(kv): attempt at making errors nice

* fix(http): return not found error if filter is invalid

* fix(http): s/platform/influxdb/ for user service

* fix(http): s/platform/influxdb/ for user service

* feat(kv): initial port of telegraf configs to kv

* feat(kv): initial port of scrapers in bolt to kv

* feat(kv): first pass at migrating bolt org service to kv

* feat(kv): first pass at bucket service

* feat(kv): first pass at migrating kvlog to kv package

* feat(kv): add resource op logs

* feat(kv): first pass at user resource mapping migration

* feat(kv): add urm usage to bucket and org services

* feat(kv): first pass at kv authz service

* feat(kv): add cascading auth delete for users

* feat(kv): first pass d authorizer.OrganizationService in kv

* feat(cmd/influxd/launcher): user kv services where appropriate

* fix(kv): initialize authorizations

* fix(influxdb): use same buckets while slowly migrating stuff

* fix(kv): make staticcheck pass

* feat(kv): add dashboards to kv

review: make suggestions from pr review

fix: use common bucket names for bolt/kv stores

* test(kv): add complete password test coverage

* chore(kv): fixes for staticcheck

* feat(kv): implement labels generically on kv

* feat(kv): implement macro service

* feat(kv): add source service

* feat(kv): add session service

* feat(kv): add kv secret service

* refactor(kv): update telegraf and urm with error messages

* feat(kv): add lookup service

* feat(kv): add kv onboarding service

* refactor(kv): update telegraf to avoid repetition

* feat(cmd/influxd): use kv lookup service

* feat(kv): add telegraf to lookup service

* feat(cmd/influxd): use kv telegraf service

* feat(kv): update scraper error messaging

* feat(cmd/influxd): add kv scraper

* feat(kv): add inmem backend tests

* refactor(kv): copy paste errors

* refactor(kv): add code to password errors

* fix(testing): update error messages for incorrect passwords

* feat(kv): rename macro to variable

* refactor(kv): auth/bucket/org/user unique checks return errors now

* feat(inmem): add way to get all bucket names from store

* feat(inmem): Buckets to return slice of bytes rather than strings

* feat(inmem): add locks around Buckets to avoid races

* feat(cmd/influx): check for unauthorized error in wrapCheckSetup

* chore(e2e): add video and screenshot artifcats to gitignore

* docs(ci): add build instructions for e2e tests

* feat(kv): add id lookup for authorized resources
pull/11959/head
Michael Desa 2019-02-19 18:47:19 -05:00 committed by Chris Goller
parent a76a2afbf8
commit 6a8a1fce32
141 changed files with 14304 additions and 1623 deletions

View File

@ -37,7 +37,46 @@ jobs:
command: bash ~/project/etc/litmus_fail_notify.sh Nightly
- store_artifacts:
path: ~/project
e2e:
docker:
- image: circleci/golang:1.11-node-browsers
environment:
GOCACHE: /tmp/go-cache
GOFLAGS: '-mod=readonly -p=4' # Go on Circle thinks 32 CPUs are available, but there aren't.
working_directory: /go/src/github.com/influxdata/influxdb
steps:
- checkout
# Speed up `make build` by restoring caches from previous runs.
- restore_cache:
name: Restoring GOCACHE
keys:
- influxdb-gocache- # Just match the most recent Go cache.
- restore_cache:
name: Restoring GOPATH/pkg/mod
keys:
- influxdb-gomod-{{ checksum "go.sum" }} # Just match the go.sum checksum cache.
- restore_cache:
name: Restore npm package cache
keys:
- chronograf-npm-packages-{{ checksum "ui/package-lock.json" }}
- run: sudo apt-get install -y netcat-openbsd
- run: make protoc
- run: mkdir -p /home/circleci/.influxdbv2/protos
- run: make build
- run:
command: ./bin/linux/influxd --store=memory --e2e-testing=true
background: true
- run: make e2e
- store_test_results:
path: ui/junit-results
destination: junit-results
- store_artifacts:
path: ui/cypress/videos
destination: videos
- store_artifacts:
path: ui/cypress/screenshots
destination: screenshots
jstest:
docker:
- image: circleci/golang:1.11-node-browsers
@ -49,11 +88,11 @@ jobs:
- restore_cache:
name: Restore npm package cache
keys:
# Only cache on exact package-lock.json match, as in Circle's yarn example:
# Only cache on exact package-lock.json match, as in Circle's npm example:
- chronograf-npm-packages-{{ checksum "ui/package-lock.json" }}
- run: make node_modules
- save_cache:
name: Save Yarn package cache
name: Save npm package cache
key: chronograf-npm-packages-{{ checksum "ui/package-lock.json" }}
paths:
- ~/.cache/npm
@ -66,7 +105,7 @@ jobs:
- image: circleci/golang:1.11
environment:
GOCACHE: /tmp/go-cache
GOFLAGS: "-mod=readonly -p=2" # Go on Circle thinks 32 CPUs are available, but there aren't.
GOFLAGS: '-mod=readonly -p=2' # Go on Circle thinks 32 CPUs are available, but there aren't.
working_directory: /go/src/github.com/influxdata/influxdb
steps:
- checkout
@ -76,13 +115,13 @@ jobs:
name: Restoring GOCACHE
keys:
- influxdb-gocache-{{ .Branch }}-{{ .Revision }} # Matches when retrying a single run.
- influxdb-gocache-{{ .Branch }}- # Matches a new commit on an existing branch.
- influxdb-gocache- # Matches a new branch.
- influxdb-gocache-{{ .Branch }}- # Matches a new commit on an existing branch.
- influxdb-gocache- # Matches a new branch.
# Populate GOPATH/pkg.
- restore_cache:
name: Restoring GOPATH/pkg/mod
keys:
- influxdb-gomod-{{ checksum "go.sum" }} # Matches based on go.sum checksum.
- influxdb-gomod-{{ checksum "go.sum" }} # Matches based on go.sum checksum.
- run: make test-go # This uses the test cache so it may succeed or fail quickly.
- run: make vet
- run: make checkfmt
@ -118,7 +157,7 @@ jobs:
- image: circleci/golang:1.11-node-browsers
environment:
GOCACHE: /tmp/go-cache
GOFLAGS: "-mod=readonly -p=4" # Go on Circle thinks 32 CPUs are available, but there aren't.
GOFLAGS: '-mod=readonly -p=4' # Go on Circle thinks 32 CPUs are available, but there aren't.
working_directory: /go/src/github.com/influxdata/influxdb
steps:
- checkout
@ -133,13 +172,15 @@ jobs:
keys:
- influxdb-gomod-{{ checksum "go.sum" }} # Just match the go.sum checksum cache.
- restore_cache:
name: Restore Yarn package cache
name: Restore npm package cache
keys:
- chronograf-npm-packages-{{ checksum "ui/package-lock.json" }}
- run: make protoc
- run: make build
- persist_to_workspace:
root: .
paths:
- project
- bin/linux/influxd
- bin/linux/influx
- etc/litmus_success_notify.sh
@ -150,7 +191,7 @@ jobs:
- image: circleci/golang:1.11-node-browsers
environment:
GOCACHE: /tmp/go-cache
GOFLAGS: "-mod=readonly -p=4" # Go on Circle thinks 32 CPUs are available, but there aren't.
GOFLAGS: '-mod=readonly -p=4' # Go on Circle thinks 32 CPUs are available, but there aren't.
working_directory: /go/src/github.com/influxdata/influxdb
steps:
- checkout
@ -165,28 +206,28 @@ jobs:
keys:
- influxdb-gomod-{{ checksum "go.sum" }} # Just match the go.sum checksum cache.
- restore_cache:
name: Restore Yarn package cache
name: Restore npm package cache
keys:
- chronograf-npm-packages-{{ checksum "ui/package-lock.json" }}
- setup_remote_docker
- run:
name: "Docker Login"
name: 'Docker Login'
command: docker login -u "$QUAY_USER" -p $QUAY_PASS quay.io
- run:
name: "Build nightly"
name: 'Build nightly'
command: make nightly
- persist_to_workspace:
root: .
paths:
- etc/litmus_success_notify.sh
- etc/litmus_fail_notify.sh
- etc/litmus_fail_notify.sh
release:
docker:
- image: circleci/golang:1.11-node-browsers
environment:
GOCACHE: /tmp/go-cache
GOFLAGS: "-mod=readonly -p=4" # Go on Circle thinks 32 CPUs are available, but there aren't.
GOFLAGS: '-mod=readonly -p=4' # Go on Circle thinks 32 CPUs are available, but there aren't.
DOCKER_VERSION: 2.0.0-alpha
working_directory: /go/src/github.com/influxdata/influxdb
steps:
@ -207,10 +248,10 @@ jobs:
- chronograf-npm-packages-{{ checksum "ui/package-lock.json" }}
- setup_remote_docker
- run:
name: "Docker Login"
name: 'Docker Login'
command: docker login -u "$QUAY_USER" -p $QUAY_PASS quay.io
- run:
name: "Build release"
name: 'Build release'
command: make release
workflows:
@ -224,10 +265,13 @@ workflows:
requires:
- build
e2e:
jobs:
- e2e
nightly:
triggers:
- schedule:
cron: "0 7 * * *"
cron: '0 7 * * *'
filters:
branches:
only:

7
.gitignore vendored
View File

@ -27,6 +27,10 @@ ui/yarn.lock
ui/build
ui/.cache
# e2e test artifacts
ui/cypress/screenshots
ui/cypress/videos
ui/src/api/.gitignore
ui/src/api/.openapi-generator-ignore
ui/src/api/.openapi-generator/VERSION
@ -118,6 +122,9 @@ man/*.1.gz
# test outputs
/test-results.xml
junit-results
cypress/screenshots
cypress/videos
# profile data
/prof

View File

@ -69,6 +69,13 @@ $(CMDS): $(SOURCES)
node_modules: ui/node_modules
# phony target to wait for server to be alive
ping:
./etc/pinger.sh
e2e: ping
make -C ui e2e
chronograf_lint:
make -C ui lint
@ -150,8 +157,16 @@ chronogiraffe: subdirs generate $(CMDS)
@echo "$$CHRONOGIRAFFE"
run: chronogiraffe
./bin/$(GOOS)/influxd --developer-mode=true
./bin/$(GOOS)/influxd --assets-path=ui/build
run-e2e: chronogiraffe
./bin/$(GOOS)/influxd --assets-path=ui/build --e2e-testing --store=memory
# assume this is running from circleci
protoc:
curl -s -L https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip > /tmp/protoc.zip
unzip -o -d /go /tmp/protoc.zip
chmod +x /go/bin/protoc
# .PHONY targets represent actions that do not create an actual file.
.PHONY: all subdirs $(SUBDIRS) run fmt checkfmt tidy checktidy checkgenerate test test-go test-js test-go-race bench clean node_modules vet nightly chronogiraffe dist
.PHONY: all subdirs $(SUBDIRS) run fmt checkfmt tidy checktidy checkgenerate test test-go test-js test-go-race bench clean node_modules vet nightly chronogiraffe dist ping protoc e2e run-e2e

View File

@ -225,7 +225,14 @@ This is problematic because it will be erased if the file is re-generated.
Until a better solution comes about, below is the list of generated files that need an ignores comment.
If you re-generate a file and find that `staticcheck` has failed, please see this list below for what you need to put back:
| File | Comment |
|:-:|:-:|
| query/promql/promql.go | //lint:file-ignore SA6001 Ignore all unused code, it's generated |
| proto/bin_gen.go | //lint:file-ignore ST1005 Ignore error strings should not be capitalized |
| File | Comment |
| :--------------------: | :----------------------------------------------------------------------: |
| query/promql/promql.go | //lint:file-ignore SA6001 Ignore all unused code, it's generated |
| proto/bin_gen.go | //lint:file-ignore ST1005 Ignore error strings should not be capitalized |
#### End-to-End Tests
CI also runs end-to-end tests. These test the integration between the influx server the ui. You can run them locally in two steps:
- Start the server in "testing mode" by running `make run-e2e`.
- Run the tests with `make e2e`.

View File

@ -67,20 +67,6 @@ func (s *TelegrafConfigService) FindTelegrafConfigByID(ctx context.Context, id i
return tc, nil
}
// FindTelegrafConfig retrieves the telegraf config and checks to see if the authorizer on context has read access to the telegraf config.
func (s *TelegrafConfigService) FindTelegrafConfig(ctx context.Context, filter influxdb.TelegrafConfigFilter) (*influxdb.TelegrafConfig, error) {
tc, err := s.s.FindTelegrafConfig(ctx, filter)
if err != nil {
return nil, err
}
if err := authorizeReadTelegraf(ctx, tc.OrganizationID, tc.ID); err != nil {
return nil, err
}
return tc, nil
}
// FindTelegrafConfigs retrieves all telegraf configs that match the provided filter and then filters the list down to only the resources that are authorized.
func (s *TelegrafConfigService) FindTelegrafConfigs(ctx context.Context, filter influxdb.TelegrafConfigFilter, opt ...influxdb.FindOptions) ([]*influxdb.TelegrafConfig, int, error) {
// TODO: we'll likely want to push this operation into the database eventually since fetching the whole list of data

View File

@ -115,91 +115,6 @@ func TestTelegrafConfigStore_FindTelegrafConfigByID(t *testing.T) {
}
}
func TestTelegrafConfigStore_FindTelegrafConfig(t *testing.T) {
type fields struct {
TelegrafConfigStore influxdb.TelegrafConfigStore
}
type args struct {
permission influxdb.Permission
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized to access telegraf",
fields: fields{
TelegrafConfigStore: &mock.TelegrafConfigStore{
FindTelegrafConfigF: func(ctx context.Context, filter influxdb.TelegrafConfigFilter) (*influxdb.TelegrafConfig, error) {
return &influxdb.TelegrafConfig{
ID: 1,
OrganizationID: 10,
}, nil
},
},
},
args: args{
permission: influxdb.Permission{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.TelegrafsResourceType,
ID: influxdbtesting.IDPtr(1),
},
},
},
wants: wants{
err: nil,
},
},
{
name: "unauthorized to access telegraf",
fields: fields{
TelegrafConfigStore: &mock.TelegrafConfigStore{
FindTelegrafConfigF: func(ctx context.Context, filter influxdb.TelegrafConfigFilter) (*influxdb.TelegrafConfig, error) {
return &influxdb.TelegrafConfig{
ID: 1,
OrganizationID: 10,
}, nil
},
},
},
args: args{
permission: influxdb.Permission{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.TelegrafsResourceType,
ID: influxdbtesting.IDPtr(2),
},
},
},
wants: wants{
err: &influxdb.Error{
Msg: "read:orgs/000000000000000a/telegrafs/0000000000000001 is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := authorizer.NewTelegrafConfigService(tt.fields.TelegrafConfigStore, mock.NewUserResourceMappingService())
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, &Authorizer{[]influxdb.Permission{tt.args.permission}})
_, err := s.FindTelegrafConfig(ctx, influxdb.TelegrafConfigFilter{})
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}
func TestTelegrafConfigStore_FindTelegrafConfigs(t *testing.T) {
type fields struct {
TelegrafConfigStore influxdb.TelegrafConfigStore

View File

@ -14,8 +14,8 @@ import (
)
var (
keyValueLogBucket = []byte("keyvaluelog/v1")
keyValueLogIndex = []byte("keyvaluelogindex/v1")
keyValueLogBucket = []byte("keyvaluelogv1")
keyValueLogIndex = []byte("keyvaluelogindexv1")
)
var _ platform.KeyValueLog = (*Client)(nil)

View File

@ -58,6 +58,34 @@ func (s *KVStore) Close() error {
return nil
}
// Flush removes all bolt keys within each bucket.
func (s *KVStore) Flush() {
_ = s.db.Update(
func(tx *bolt.Tx) error {
return tx.ForEach(func(name []byte, b *bolt.Bucket) error {
s.cleanBucket(tx, b)
return nil
})
},
)
}
func (s *KVStore) cleanBucket(tx *bolt.Tx, b *bolt.Bucket) {
// nested bucket recursion base case:
if b == nil {
return
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
_ = v
if err := c.Delete(); err != nil {
// clean out nexted buckets
s.cleanBucket(tx, b.Bucket(k))
}
}
}
// WithLogger sets the logger on the store.
func (s *KVStore) WithLogger(l *zap.Logger) {
s.logger = l
@ -176,7 +204,7 @@ type Cursor struct {
// Seek seeks for the first key that matches the prefix provided.
func (c *Cursor) Seek(prefix []byte) ([]byte, []byte) {
k, v := c.cursor.Seek(prefix)
if len(v) == 0 {
if len(k) == 0 && len(v) == 0 {
return nil, nil
}
return k, v
@ -185,7 +213,7 @@ func (c *Cursor) Seek(prefix []byte) ([]byte, []byte) {
// First retrieves the first key value pair in the bucket.
func (c *Cursor) First() ([]byte, []byte) {
k, v := c.cursor.First()
if len(v) == 0 {
if len(k) == 0 && len(v) == 0 {
return nil, nil
}
return k, v
@ -194,7 +222,7 @@ func (c *Cursor) First() ([]byte, []byte) {
// Last retrieves the last key value pair in the bucket.
func (c *Cursor) Last() ([]byte, []byte) {
k, v := c.cursor.Last()
if len(v) == 0 {
if len(k) == 0 && len(v) == 0 {
return nil, nil
}
return k, v
@ -203,7 +231,7 @@ func (c *Cursor) Last() ([]byte, []byte) {
// Next retrieves the next key in the bucket.
func (c *Cursor) Next() ([]byte, []byte) {
k, v := c.cursor.Next()
if len(v) == 0 {
if len(k) == 0 && len(v) == 0 {
return nil, nil
}
return k, v
@ -212,7 +240,7 @@ func (c *Cursor) Next() ([]byte, []byte) {
// Prev retrieves the previous key in the bucket.
func (c *Cursor) Prev() ([]byte, []byte) {
k, v := c.cursor.Prev()
if len(v) == 0 {
if len(k) == 0 && len(v) == 0 {
return nil, nil
}
return k, v

View File

@ -1,10 +1,8 @@
package bolt_test
import (
"context"
"testing"
platform "github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
platformtesting "github.com/influxdata/influxdb/testing"
)
@ -40,53 +38,3 @@ func initKVStore(f platformtesting.KVStoreFields, t *testing.T) (kv.Store, func(
func TestKVStore(t *testing.T) {
platformtesting.KVStore(initKVStore, t)
}
func initExampleService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) {
s, closeFn, err := NewTestKVStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc := kv.NewExampleService(s, f.IDGenerator)
if err := svc.Initialize(); err != nil {
t.Fatalf("error initializing user service: %v", err)
}
ctx := context.Background()
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
return svc, "kv/", func() {
defer closeFn()
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove users: %v", err)
}
}
}
}
func TestExampleService_CreateUser(t *testing.T) {
platformtesting.CreateUser(initExampleService, t)
}
func TestExampleService_FindUserByID(t *testing.T) {
platformtesting.FindUserByID(initExampleService, t)
}
func TestExampleService_FindUsers(t *testing.T) {
platformtesting.FindUsers(initExampleService, t)
}
func TestExampleService_DeleteUser(t *testing.T) {
platformtesting.DeleteUser(initExampleService, t)
}
func TestExampleService_FindUser(t *testing.T) {
platformtesting.FindUser(initExampleService, t)
}
func TestExampleService_UpdateUser(t *testing.T) {
platformtesting.UpdateUser(initExampleService, t)
}

View File

@ -54,7 +54,7 @@ func TestMetrics_Onboarding(t *testing.T) {
if _, _ = client.Generate(ctx,
&platform.OnboardingRequest{
User: "u1",
Password: "p1",
Password: "password1",
Org: "o1",
Bucket: "b1",
}); err != nil {

View File

@ -5,7 +5,7 @@ import (
"fmt"
"time"
"github.com/coreos/bbolt"
bolt "github.com/coreos/bbolt"
platform "github.com/influxdata/influxdb"
)

View File

@ -2,11 +2,42 @@ package bolt
import (
"context"
"fmt"
bolt "github.com/coreos/bbolt"
platform "github.com/influxdata/influxdb"
"golang.org/x/crypto/bcrypt"
)
// MinPasswordLength is the shortest password we allow into the system.
const MinPasswordLength = 8
var (
// EIncorrectPassword is returned when any password operation fails in which
// we do not want to leak information.
EIncorrectPassword = &platform.Error{
Msg: "<forbidden> your username or password is incorrect",
}
// EShortPassword is used when a password is less than the minimum
// acceptable password length.
EShortPassword = &platform.Error{
Msg: "<invalid> passwords are required to be longer than 8 characters",
}
)
// CorruptUserIDError is used when the ID was encoded incorrectly previously.
// This is some sort of internal server error.
func CorruptUserIDError(name string, err error) error {
return &platform.Error{
Code: platform.EInternal,
Msg: fmt.Sprintf("User ID for %s has been corrupted; Err: %v", name, err),
Op: "bolt/setPassword",
}
}
var _ platform.PasswordsService = (*Client)(nil)
// SetPassword stores the password hash associated with a user.
func (c *Client) SetPassword(ctx context.Context, name string, password string) error {
return c.db.Update(func(tx *bolt.Tx) error {
@ -18,6 +49,10 @@ func (c *Client) SetPassword(ctx context.Context, name string, password string)
var HashCost = bcrypt.DefaultCost
func (c *Client) setPassword(ctx context.Context, tx *bolt.Tx, name string, password string) error {
if len(password) < MinPasswordLength {
return EShortPassword
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), HashCost)
if err != nil {
return err
@ -25,12 +60,12 @@ func (c *Client) setPassword(ctx context.Context, tx *bolt.Tx, name string, pass
u, pe := c.findUserByName(ctx, tx, name)
if pe != nil {
return pe
return EIncorrectPassword
}
encodedID, err := u.ID.Encode()
if err != nil {
return err
return CorruptUserIDError(name, err)
}
return tx.Bucket(userpasswordBucket).Put(encodedID, hash)
@ -55,7 +90,11 @@ func (c *Client) comparePassword(ctx context.Context, tx *bolt.Tx, name string,
hash := tx.Bucket(userpasswordBucket).Get(encodedID)
return bcrypt.CompareHashAndPassword(hash, []byte(password))
if err := bcrypt.CompareHashAndPassword(hash, []byte(password)); err != nil {
// User exists but the password was incorrect
return EIncorrectPassword
}
return nil
}
// CompareAndSetPassword replaces the old password with the new password if thee old password is correct.

View File

@ -8,7 +8,7 @@ import (
platformtesting "github.com/influxdata/influxdb/testing"
)
func initBasicAuthService(f platformtesting.UserFields, t *testing.T) (platform.BasicAuthService, func()) {
func initPasswordsService(f platformtesting.PasswordFields, t *testing.T) (platform.PasswordsService, func()) {
c, closeFn, err := NewTestClient()
if err != nil {
t.Fatalf("failed to create new bolt client: %v", err)
@ -20,6 +20,13 @@ func initBasicAuthService(f platformtesting.UserFields, t *testing.T) (platform.
t.Fatalf("failed to populate users")
}
}
for i := range f.Passwords {
if err := c.SetPassword(ctx, f.Users[i].Name, f.Passwords[i]); err != nil {
t.Fatalf("error setting passsword user, %s %s: %v", f.Users[i].Name, f.Passwords[i], err)
}
}
return c, func() {
defer closeFn()
for _, u := range f.Users {
@ -30,12 +37,12 @@ func initBasicAuthService(f platformtesting.UserFields, t *testing.T) (platform.
}
}
func TestBasicAuth(t *testing.T) {
func TestPasswords(t *testing.T) {
t.Parallel()
platformtesting.BasicAuth(initBasicAuthService, t)
platformtesting.SetPassword(initPasswordsService, t)
}
func TestBasicAuth_CompareAndSet(t *testing.T) {
func TestPasswords_CompareAndSet(t *testing.T) {
t.Parallel()
platformtesting.CompareAndSetPassword(initBasicAuthService, t)
platformtesting.CompareAndSetPassword(initPasswordsService, t)
}

View File

@ -49,14 +49,14 @@ func (c *Client) AddTarget(ctx context.Context, target *platform.ScraperTarget,
if !target.OrgID.Valid() {
return &platform.Error{
Code: platform.EInvalid,
Msg: "org id is invalid",
Msg: "provided organization ID has invalid format",
Op: OpPrefix + platform.OpAddTarget,
}
}
if !target.BucketID.Valid() {
return &platform.Error{
Code: platform.EInvalid,
Msg: "bucket id is invalid",
Msg: "provided bucket ID has invalid format",
Op: OpPrefix + platform.OpAddTarget,
}
}
@ -121,7 +121,7 @@ func (c *Client) UpdateTarget(ctx context.Context, update *platform.ScraperTarge
return nil, &platform.Error{
Code: platform.EInvalid,
Op: op,
Msg: "id is invalid",
Msg: "provided scraper target ID has invalid format",
}
}
err = c.db.Update(func(tx *bolt.Tx) error {

View File

@ -23,15 +23,13 @@ func (c *Client) initializeTelegraf(ctx context.Context, tx *bolt.Tx) error {
// FindTelegrafConfigByID returns a single telegraf config by ID.
func (c *Client) FindTelegrafConfigByID(ctx context.Context, id platform.ID) (tc *platform.TelegrafConfig, err error) {
op := OpPrefix + platform.OpFindTelegrafConfigByID
err = c.db.View(func(tx *bolt.Tx) error {
var pErr *platform.Error
tc, pErr = c.findTelegrafConfigByID(ctx, tx, id)
if pErr != nil {
pErr.Op = op
err = pErr
var pe *platform.Error
tc, pe = c.findTelegrafConfigByID(ctx, tx, id)
if pe != nil {
return pe
}
return err
return nil
})
return tc, err
}
@ -40,8 +38,8 @@ func (c *Client) findTelegrafConfigByID(ctx context.Context, tx *bolt.Tx, id pla
encID, err := id.Encode()
if err != nil {
return nil, &platform.Error{
Code: platform.EEmptyValue,
Err: err,
Code: platform.EInvalid,
Msg: "provided telegraf configuration ID has invalid format",
}
}
d := tx.Bucket(telegrafBucket).Get(encID)
@ -61,22 +59,6 @@ func (c *Client) findTelegrafConfigByID(ctx context.Context, tx *bolt.Tx, id pla
return tc, nil
}
// FindTelegrafConfig returns the first telegraf config that matches filter.
func (c *Client) FindTelegrafConfig(ctx context.Context, filter platform.TelegrafConfigFilter) (*platform.TelegrafConfig, error) {
op := OpPrefix + platform.OpFindTelegrafConfig
tcs, n, err := c.FindTelegrafConfigs(ctx, filter, platform.FindOptions{Limit: 1})
if err != nil {
return nil, err
}
if n > 0 {
return tcs[0], nil
}
return nil, &platform.Error{
Code: platform.ENotFound,
Op: op,
}
}
func (c *Client) findTelegrafConfigs(ctx context.Context, tx *bolt.Tx, filter platform.TelegrafConfigFilter, opt ...platform.FindOptions) ([]*platform.TelegrafConfig, int, *platform.Error) {
tcs := make([]*platform.TelegrafConfig, 0)
m, err := c.findUserResourceMappings(ctx, tx, filter.UserResourceMappingFilter)
@ -205,13 +187,12 @@ func (c *Client) UpdateTelegrafConfig(ctx context.Context, id platform.ID, tc *p
// DeleteTelegrafConfig removes a telegraf config by ID.
func (c *Client) DeleteTelegrafConfig(ctx context.Context, id platform.ID) error {
op := OpPrefix + platform.OpDeleteTelegrafConfig
err := c.db.Update(func(tx *bolt.Tx) error {
encodedID, err := id.Encode()
if err != nil {
return &platform.Error{
Code: platform.EEmptyValue,
Err: err,
Code: platform.EInvalid,
Msg: "provided telegraf configuration ID has invalid format",
}
}
err = tx.Bucket(telegrafBucket).Delete(encodedID)
@ -226,7 +207,6 @@ func (c *Client) DeleteTelegrafConfig(ctx context.Context, id platform.ID) error
if err != nil {
err = &platform.Error{
Code: platform.ErrorCode(err),
Op: op,
Err: err,
}
}

View File

@ -19,7 +19,6 @@ var (
var _ platform.UserService = (*Client)(nil)
var _ platform.UserOperationLogService = (*Client)(nil)
var _ platform.BasicAuthService = (*Client)(nil)
func (c *Client) initializeUsers(ctx context.Context, tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(userBucket)); err != nil {
@ -119,8 +118,6 @@ func (c *Client) findUserByName(ctx context.Context, tx *bolt.Tx, n string) (*pl
}
// FindUser retrives a user using an arbitrary user filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across users until it finds a match.
func (c *Client) FindUser(ctx context.Context, filter platform.UserFilter) (*platform.User, error) {
var u *platform.User
var err error
@ -147,33 +144,10 @@ func (c *Client) FindUser(ctx context.Context, filter platform.UserFilter) (*pla
return u, nil
}
filterFn := filterUsersFn(filter)
err = c.db.View(func(tx *bolt.Tx) error {
return forEachUser(ctx, tx, func(usr *platform.User) bool {
if filterFn(usr) {
u = usr
return false
}
return true
})
})
if err != nil {
return nil, &platform.Error{
Op: op,
Err: err,
}
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "user not found",
}
if u == nil {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "user not found",
}
}
return u, nil
}
func filterUsersFn(filter platform.UserFilter) func(u *platform.User) bool {

View File

@ -72,7 +72,10 @@ func (c *Client) findUserResourceMapping(ctx context.Context, tx *bolt.Tx, filte
}
if len(ms) == 0 {
return nil, fmt.Errorf("userResource mapping not found")
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "user to resource mapping not found",
}
}
return ms[0], nil
@ -96,7 +99,10 @@ func (c *Client) createUserResourceMapping(ctx context.Context, tx *bolt.Tx, m *
unique := c.uniqueUserResourceMapping(ctx, tx, m)
if !unique {
return fmt.Errorf("mapping for user %s already exists", m.UserID.String())
return &platform.Error{
Code: platform.EInternal,
Msg: fmt.Sprintf("Unexpected error when assigning user to a resource: mapping for user %s already exists", m.UserID.String()),
}
}
v, err := json.Marshal(m)
@ -214,7 +220,10 @@ func (c *Client) deleteUserResourceMapping(ctx context.Context, tx *bolt.Tx, fil
return err
}
if len(ms) == 0 {
return fmt.Errorf("userResource mapping not found")
return &platform.Error{
Code: platform.ENotFound,
Msg: "user to resource mapping not found",
}
}
key, err := userResourceKey(ms[0])

View File

@ -4,28 +4,34 @@ import (
"context"
"testing"
platform "github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/bolt"
platformtesting "github.com/influxdata/influxdb/testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func initUserService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) {
c, closeFn, err := NewTestClient()
func initUserService(f influxdbtesting.UserFields, t *testing.T) (influxdb.UserService, string, func()) {
svc, closeFn, err := NewTestClient()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
c.IDGenerator = f.IDGenerator
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
/*
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing user service: %v", err)
}
*/
for _, u := range f.Users {
if err := c.PutUser(ctx, u); err != nil {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
return c, bolt.OpPrefix, func() {
return svc, kv.OpPrefix, func() {
defer closeFn()
for _, u := range f.Users {
if err := c.DeleteUser(ctx, u.ID); err != nil {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove users: %v", err)
}
}
@ -33,5 +39,5 @@ func initUserService(f platformtesting.UserFields, t *testing.T) (platform.UserS
}
func TestUserService(t *testing.T) {
platformtesting.UserService(initUserService, t)
influxdbtesting.UserService(initUserService, t)
}

View File

@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/cmd/influx/internal"
"github.com/influxdata/influxdb/http"
"github.com/influxdata/influxdb/internal/fs"
@ -116,7 +117,7 @@ func wrapCheckSetup(fn func(*cobra.Command, []string) error) func(*cobra.Command
return nil
}
if setupErr := checkSetup(flags.host); setupErr != nil {
if setupErr := checkSetup(flags.host); setupErr != nil && influxdb.EUnauthorized != influxdb.ErrorCode(setupErr) {
return setupErr
}

View File

@ -20,9 +20,11 @@ import (
protofs "github.com/influxdata/influxdb/fs"
"github.com/influxdata/influxdb/gather"
"github.com/influxdata/influxdb/http"
"github.com/influxdata/influxdb/inmem"
"github.com/influxdata/influxdb/internal/fs"
"github.com/influxdata/influxdb/kit/cli"
"github.com/influxdata/influxdb/kit/prom"
"github.com/influxdata/influxdb/kv"
influxlogger "github.com/influxdata/influxdb/logger"
"github.com/influxdata/influxdb/nats"
infprom "github.com/influxdata/influxdb/prometheus"
@ -48,13 +50,23 @@ import (
"go.uber.org/zap/zapcore"
)
const (
// BoltStore stores all REST resources in boltdb.
BoltStore = "bolt"
// MemoryStore stores all REST resources in memory (useful for testing).
MemoryStore = "memory"
)
// Launcher represents the main program execution.
type Launcher struct {
wg sync.WaitGroup
cancel func()
running bool
developerMode bool
storeType string
assetsPath string
testing bool
logLevel string
reportingDisabled bool
@ -65,6 +77,7 @@ type Launcher struct {
secretStore string
boltClient *bolt.Client
kvService *kv.Service
engine *storage.Engine
queryController *pcontrol.Controller
@ -199,10 +212,21 @@ func (m *Launcher) Run(ctx context.Context, args ...string) error {
Desc: "path to boltdb database",
},
{
DestP: &m.developerMode,
Flag: "developer-mode",
DestP: &m.assetsPath,
Flag: "assets-path",
Desc: "override default assets by serving from a specific directory (developer mode)",
},
{
DestP: &m.storeType,
Flag: "store",
Default: "bolt",
Desc: "backing store for REST resources (bolt or memory)",
},
{
DestP: &m.testing,
Flag: "e2e-testing",
Default: false,
Desc: "serve assets from the local filesystem in developer mode",
Desc: "add /debug/flush endpoint to clear stores; used for end-to-end tests",
},
{
DestP: &m.enginePath,
@ -276,6 +300,33 @@ func (m *Launcher) run(ctx context.Context) (err error) {
return err
}
var flusher http.Flusher
switch m.storeType {
case BoltStore:
store := bolt.NewKVStore(m.boltPath)
store.WithDB(m.boltClient.DB())
m.kvService = kv.NewService(store)
if m.testing {
flusher = store
}
case MemoryStore:
store := inmem.NewKVStore()
m.kvService = kv.NewService(store)
if m.testing {
flusher = store
}
default:
err := fmt.Errorf("unknown store type %s; expected bolt or memory", m.storeType)
m.logger.Error("failed opening bolt", zap.Error(err))
return err
}
m.kvService.Logger = m.logger.With(zap.String("store", "kv"))
if err := m.kvService.Initialize(ctx); err != nil {
m.logger.Error("failed to initialize kv service", zap.Error(err))
return err
}
m.reg = prom.NewRegistry()
m.reg.MustRegister(
prometheus.NewGoCollector(),
@ -285,26 +336,26 @@ func (m *Launcher) run(ctx context.Context) (err error) {
m.reg.MustRegister(m.boltClient)
var (
orgSvc platform.OrganizationService = m.boltClient
authSvc platform.AuthorizationService = m.boltClient
userSvc platform.UserService = m.boltClient
variableSvc platform.VariableService = m.boltClient
bucketSvc platform.BucketService = m.boltClient
sourceSvc platform.SourceService = m.boltClient
sessionSvc platform.SessionService = m.boltClient
basicAuthSvc platform.BasicAuthService = m.boltClient
dashboardSvc platform.DashboardService = m.boltClient
dashboardLogSvc platform.DashboardOperationLogService = m.boltClient
userLogSvc platform.UserOperationLogService = m.boltClient
bucketLogSvc platform.BucketOperationLogService = m.boltClient
orgLogSvc platform.OrganizationOperationLogService = m.boltClient
onboardingSvc platform.OnboardingService = m.boltClient
scraperTargetSvc platform.ScraperTargetStoreService = m.boltClient
telegrafSvc platform.TelegrafConfigStore = m.boltClient
userResourceSvc platform.UserResourceMappingService = m.boltClient
labelSvc platform.LabelService = m.boltClient
secretSvc platform.SecretService = m.boltClient
lookupSvc platform.LookupService = m.boltClient
orgSvc platform.OrganizationService = m.kvService
authSvc platform.AuthorizationService = m.kvService
userSvc platform.UserService = m.kvService
variableSvc platform.VariableService = m.kvService
bucketSvc platform.BucketService = m.kvService
sourceSvc platform.SourceService = m.kvService
sessionSvc platform.SessionService = m.kvService
passwdsSvc platform.PasswordsService = m.kvService
dashboardSvc platform.DashboardService = m.kvService
dashboardLogSvc platform.DashboardOperationLogService = m.kvService
userLogSvc platform.UserOperationLogService = m.kvService
bucketLogSvc platform.BucketOperationLogService = m.kvService
orgLogSvc platform.OrganizationOperationLogService = m.kvService
onboardingSvc platform.OnboardingService = m.kvService
scraperTargetSvc platform.ScraperTargetStoreService = m.kvService
telegrafSvc platform.TelegrafConfigStore = m.kvService
userResourceSvc platform.UserResourceMappingService = m.kvService
labelSvc platform.LabelService = m.kvService
secretSvc platform.SecretService = m.kvService
lookupSvc platform.LookupService = m.kvService
)
switch m.secretStore {
@ -387,24 +438,32 @@ func (m *Launcher) run(ctx context.Context) (err error) {
var storageQueryService = readservice.NewProxyQueryService(m.queryController)
var taskSvc platform.TaskService
{
boltStore, err := taskbolt.New(m.boltClient.DB(), "tasks")
var (
store taskbackend.Store
err error
)
store, err = taskbolt.New(m.boltClient.DB(), "tasks")
if err != nil {
m.logger.Error("failed opening task bolt", zap.Error(err))
return err
}
executor := taskexecutor.NewAsyncQueryServiceExecutor(m.logger.With(zap.String("service", "task-executor")), m.queryController, authSvc, boltStore)
if m.storeType == "memory" {
store = taskbackend.NewInMemStore()
}
executor := taskexecutor.NewAsyncQueryServiceExecutor(m.logger.With(zap.String("service", "task-executor")), m.queryController, authSvc, store)
lw := taskbackend.NewPointLogWriter(pointsWriter)
m.scheduler = taskbackend.NewScheduler(boltStore, executor, lw, time.Now().UTC().Unix(), taskbackend.WithTicker(ctx, 100*time.Millisecond), taskbackend.WithLogger(m.logger))
m.scheduler = taskbackend.NewScheduler(store, executor, lw, time.Now().UTC().Unix(), taskbackend.WithTicker(ctx, 100*time.Millisecond), taskbackend.WithLogger(m.logger))
m.scheduler.Start(ctx)
m.reg.MustRegister(m.scheduler.PrometheusCollectors()...)
queryService := query.QueryServiceBridge{AsyncQueryService: m.queryController}
lr := taskbackend.NewQueryLogReader(queryService)
taskSvc = task.PlatformAdapter(coordinator.New(m.logger.With(zap.String("service", "task-coordinator")), m.scheduler, boltStore), lr, m.scheduler, authSvc, userResourceSvc)
taskSvc = task.PlatformAdapter(coordinator.New(m.logger.With(zap.String("service", "task-coordinator")), m.scheduler, store), lr, m.scheduler, authSvc, userResourceSvc)
taskSvc = task.NewValidator(taskSvc, bucketSvc)
m.taskStore = boltStore
m.taskStore = store
}
// NATS streaming server
@ -454,7 +513,7 @@ func (m *Launcher) run(ctx context.Context) (err error) {
}
m.apibackend = &http.APIBackend{
DeveloperMode: m.developerMode,
AssetsPath: m.assetsPath,
Logger: m.logger,
NewBucketService: source.NewBucketService,
NewQueryService: source.NewQueryService,
@ -474,7 +533,7 @@ func (m *Launcher) run(ctx context.Context) (err error) {
OrganizationOperationLogService: orgLogSvc,
SourceService: sourceSvc,
VariableService: variableSvc,
BasicAuthService: basicAuthSvc,
PasswordsService: passwdsSvc,
OnboardingService: onboardingSvc,
ProxyQueryService: storageQueryService,
TaskService: taskSvc,
@ -484,7 +543,7 @@ func (m *Launcher) run(ctx context.Context) (err error) {
SecretService: secretSvc,
LookupService: lookupSvc,
ProtoService: protoSvc,
OrgLookupService: m.boltClient,
OrgLookupService: m.kvService,
}
// HTTP server
@ -498,6 +557,10 @@ func (m *Launcher) run(ctx context.Context) (err error) {
h.Tracer = opentracing.GlobalTracer()
m.httpServer.Handler = h
// If we are in testing mode we allow all data to be flushed and removed.
if m.testing {
m.httpServer.Handler = http.DebugFlush(h, flusher)
}
ln, err := net.Listen("tcp", m.httpBindAddress)
if err != nil {
@ -524,34 +587,42 @@ func (m *Launcher) run(ctx context.Context) (err error) {
return nil
}
// OrganizationService returns the internal organization service.
func (m *Launcher) OrganizationService() platform.OrganizationService {
return m.apibackend.OrganizationService
}
// QueryController returns the internal query service.
func (m *Launcher) QueryController() *pcontrol.Controller {
return m.queryController
}
// BucketService returns the internal bucket service.
func (m *Launcher) BucketService() platform.BucketService {
return m.apibackend.BucketService
}
// UserService returns the internal suser service.
func (m *Launcher) UserService() platform.UserService {
return m.apibackend.UserService
}
// AuthorizationService returns the internal authorization service.
func (m *Launcher) AuthorizationService() platform.AuthorizationService {
return m.apibackend.AuthorizationService
}
// TaskService returns the internal task service.
func (m *Launcher) TaskService() platform.TaskService {
return m.apibackend.TaskService
}
// TaskStore returns the internal store service.
func (m *Launcher) TaskStore() taskbackend.Store {
return m.taskStore
}
// TaskScheduler returns the internal scheduler service.
func (m *Launcher) TaskScheduler() taskbackend.Scheduler {
return m.scheduler
}

6
etc/pinger.sh Executable file
View File

@ -0,0 +1,6 @@
ping_cancelled=false # Keep track of whether the loop was cancelled, or succeeded
until nc -z 127.0.0.1 9999; do :; done &
trap "kill $!; ping_cancelled=true" SIGINT
wait $! # Wait for the loop to exit, one way or another
trap - INT # Remove the trap, now we're done with it
echo "Done pinging, cancelled=$ping_cancelled"

View File

@ -38,8 +38,8 @@ type APIHandler struct {
// APIBackend is all services and associated parameters required to construct
// an APIHandler.
type APIBackend struct {
DeveloperMode bool
Logger *zap.Logger
AssetsPath string // if empty then assets are served from bindata.
Logger *zap.Logger
NewBucketService func(*influxdb.Source) (influxdb.BucketService, error)
NewQueryService func(*influxdb.Source) (query.ProxyQueryService, error)
@ -59,7 +59,7 @@ type APIBackend struct {
OrganizationOperationLogService influxdb.OrganizationOperationLogService
SourceService influxdb.SourceService
VariableService influxdb.VariableService
BasicAuthService influxdb.BasicAuthService
PasswordsService influxdb.PasswordsService
OnboardingService influxdb.OnboardingService
ProxyQueryService query.ProxyQueryService
TaskService influxdb.TaskService
@ -135,11 +135,8 @@ func NewAPIHandler(b *APIBackend) *APIHandler {
h.QueryHandler = NewFluxHandler(fluxBackend)
h.ProtoHandler = NewProtoHandler(NewProtoBackend(b))
h.ChronografHandler = NewChronografHandler(b.ChronografService)
h.SwaggerHandler = SwaggerHandler()
h.LabelHandler = NewLabelHandler(b.LabelService)
return h

View File

@ -2,6 +2,7 @@ package http
import (
"net/http"
"path/filepath"
// TODO: use platform version of the code
"github.com/influxdata/influxdb/chronograf"
@ -13,33 +14,29 @@ const (
Dir = "../../ui/build"
// Default is the default item to load if 404
Default = "../../ui/build/index.html"
// DebugDir is the prefix of the assets in development mode
DebugDir = "ui/build"
// DebugDefault is the default item to load if 404
DebugDefault = "ui/build/index.html"
DebugDefault = "index.html"
// DefaultContentType is the content-type to return for the Default file
DefaultContentType = "text/html; charset=utf-8"
)
// AssetHandler is an http handler for serving chronograf assets.
type AssetHandler struct {
DeveloperMode bool
Path string
}
// NewAssetHandler is the constructor an asset handler.
func NewAssetHandler() *AssetHandler {
return &AssetHandler{
DeveloperMode: true,
}
return &AssetHandler{}
}
// ServeHTTP implements the http handler interface for serving assets.
func (h *AssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var assets chronograf.Assets
if h.DeveloperMode {
if h.Path != "" {
assets = &dist.DebugAssets{
Dir: DebugDir,
Default: DebugDefault,
Dir: h.Path,
Default: filepath.Join(h.Path, DebugDefault),
}
} else {
assets = &dist.BindataAssets{

21
http/debug.go Normal file
View File

@ -0,0 +1,21 @@
package http
import "net/http"
// Flusher flushes data from a store to reset; used for testing.
type Flusher interface {
Flush()
}
// DebugFlush clears all services for testing.
func DebugFlush(next http.Handler, f Flusher) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/debug/flush" {
f.Flush()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}

View File

@ -39,7 +39,7 @@ func initOnboardingService(f platformtesting.OnboardingFields, t *testing.T) (pl
client := struct {
*SetupService
*Service
platform.BasicAuthService
platform.PasswordsService
}{
SetupService: &SetupService{
Addr: server.URL,
@ -47,7 +47,7 @@ func initOnboardingService(f platformtesting.OnboardingFields, t *testing.T) (pl
Service: &Service{
Addr: server.URL,
},
BasicAuthService: svc,
PasswordsService: svc,
}
done := server.Close

View File

@ -37,7 +37,7 @@ func NewPlatformHandler(b *APIBackend) *PlatformHandler {
h.RegisterNoAuthRoute("GET", "/api/v2/swagger.json")
assetHandler := NewAssetHandler()
assetHandler.DeveloperMode = b.DeveloperMode
assetHandler.Path = b.AssetsPath
return &PlatformHandler{
AssetHandler: assetHandler,

View File

@ -336,7 +336,7 @@ func (s *ScraperService) UpdateTarget(ctx context.Context, update *influxdb.Scra
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Op: s.OpPrefix + influxdb.OpUpdateTarget,
Msg: "id is invalid",
Msg: "provided scraper target ID has invalid format",
}
}
url, err := newURL(s.Addr, targetIDPath(update.ID))
@ -384,14 +384,14 @@ func (s *ScraperService) AddTarget(ctx context.Context, target *influxdb.Scraper
if !target.OrgID.Valid() {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "org id is invalid",
Msg: "provided organization ID has invalid format",
Op: s.OpPrefix + influxdb.OpAddTarget,
}
}
if !target.BucketID.Valid() {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "bucket id is invalid",
Msg: "provided bucket ID has invalid format",
Op: s.OpPrefix + influxdb.OpAddTarget,
}
}

View File

@ -14,15 +14,16 @@ import (
type SessionBackend struct {
Logger *zap.Logger
BasicAuthService platform.BasicAuthService
PasswordsService platform.PasswordsService
SessionService platform.SessionService
}
// NewSessionBackend creates a new SessionBackend with associated logger.
func NewSessionBackend(b *APIBackend) *SessionBackend {
return &SessionBackend{
Logger: b.Logger.With(zap.String("handler", "session")),
BasicAuthService: b.BasicAuthService,
PasswordsService: b.PasswordsService,
SessionService: b.SessionService,
}
}
@ -32,7 +33,7 @@ type SessionHandler struct {
*httprouter.Router
Logger *zap.Logger
BasicAuthService platform.BasicAuthService
PasswordsService platform.PasswordsService
SessionService platform.SessionService
}
@ -42,7 +43,7 @@ func NewSessionHandler(b *SessionBackend) *SessionHandler {
Router: NewRouter(),
Logger: b.Logger,
BasicAuthService: b.BasicAuthService,
PasswordsService: b.PasswordsService,
SessionService: b.SessionService,
}
@ -61,7 +62,7 @@ func (h *SessionHandler) handleSignin(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.BasicAuthService.ComparePassword(ctx, req.Username, req.Password); err != nil {
if err := h.PasswordsService.ComparePassword(ctx, req.Username, req.Password); err != nil {
// Don't log here, it should already be handled by the service
UnauthorizedError(ctx, w)
return

View File

@ -2,12 +2,13 @@ package http_test
import (
"context"
"go.uber.org/zap"
"net/http"
"net/http/httptest"
"testing"
"time"
"go.uber.org/zap"
platform "github.com/influxdata/influxdb"
platformhttp "github.com/influxdata/influxdb/http"
"github.com/influxdata/influxdb/mock"
@ -19,13 +20,13 @@ func NewMockSessionBackend() *platformhttp.SessionBackend {
Logger: zap.NewNop().With(zap.String("handler", "session")),
SessionService: mock.NewSessionService(),
BasicAuthService: mock.NewBasicAuthService("", ""),
PasswordsService: mock.NewPasswordsService("", ""),
}
}
func TestBasicAuthHandler_handleSignin(t *testing.T) {
func TestSessionHandler_handleSignin(t *testing.T) {
type fields struct {
BasicAuthService platform.BasicAuthService
PasswordsService platform.PasswordsService
SessionService platform.SessionService
}
type args struct {
@ -57,7 +58,7 @@ func TestBasicAuthHandler_handleSignin(t *testing.T) {
}, nil
},
},
BasicAuthService: &mock.BasicAuthService{
PasswordsService: &mock.PasswordsService{
ComparePasswordFn: func(context.Context, string, string) error {
return nil
},
@ -77,7 +78,7 @@ func TestBasicAuthHandler_handleSignin(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := NewMockSessionBackend()
b.BasicAuthService = tt.fields.BasicAuthService
b.PasswordsService = tt.fields.PasswordsService
b.SessionService = tt.fields.SessionService
h := platformhttp.NewSessionHandler(b)

View File

@ -6833,6 +6833,7 @@ components:
type: object
properties:
allowed:
description: true means that the influxdb instance has NOT had initial setup; false means that the database has been setup.
type: boolean
OnboardingRequest:
type: object

View File

@ -5,43 +5,41 @@ import (
"context"
"encoding/json"
"fmt"
"go.uber.org/zap"
"net/http"
"path"
platform "github.com/influxdata/influxdb"
platcontext "github.com/influxdata/influxdb/context"
"github.com/influxdata/influxdb"
icontext "github.com/influxdata/influxdb/context"
"github.com/julienschmidt/httprouter"
"go.uber.org/zap"
)
// UserBackend is all services and associated parameters required to construct
// the UserHandler.
type UserBackend struct {
Logger *zap.Logger
UserService platform.UserService
UserOperationLogService platform.UserOperationLogService
BasicAuthService platform.BasicAuthService
Logger *zap.Logger
UserService influxdb.UserService
UserOperationLogService influxdb.UserOperationLogService
PasswordsService influxdb.PasswordsService
}
// NewUserBackend creates a UserBackend using information in the APIBackend.
func NewUserBackend(b *APIBackend) *UserBackend {
return &UserBackend{
Logger: b.Logger.With(zap.String("handler", "user")),
Logger: b.Logger.With(zap.String("handler", "user")),
UserService: b.UserService,
UserOperationLogService: b.UserOperationLogService,
BasicAuthService: b.BasicAuthService,
PasswordsService: b.PasswordsService,
}
}
// UserHandler represents an HTTP API handler for users.
type UserHandler struct {
*httprouter.Router
Logger *zap.Logger
UserService platform.UserService
UserOperationLogService platform.UserOperationLogService
BasicAuthService platform.BasicAuthService
Logger *zap.Logger
UserService influxdb.UserService
UserOperationLogService influxdb.UserOperationLogService
PasswordsService influxdb.PasswordsService
}
const (
@ -61,7 +59,7 @@ func NewUserHandler(b *UserBackend) *UserHandler {
UserService: b.UserService,
UserOperationLogService: b.UserOperationLogService,
BasicAuthService: b.BasicAuthService,
PasswordsService: b.PasswordsService,
}
h.HandlerFunc("POST", usersPath, h.handlePostUser)
@ -85,7 +83,7 @@ func (h *UserHandler) putPassword(ctx context.Context, w http.ResponseWriter, r
return "", err
}
err = h.BasicAuthService.CompareAndSetPassword(ctx, req.Username, req.PasswordOld, req.PasswordNew)
err = h.PasswordsService.CompareAndSetPassword(ctx, req.Username, req.PasswordOld, req.PasswordNew)
if err != nil {
return "", err
}
@ -100,7 +98,7 @@ func (h *UserHandler) handlePutUserPassword(w http.ResponseWriter, r *http.Reque
EncodeError(ctx, err, w)
return
}
filter := platform.UserFilter{
filter := influxdb.UserFilter{
Name: &username,
}
b, err := h.UserService.FindUser(ctx, filter)
@ -134,8 +132,8 @@ func decodePasswordResetRequest(ctx context.Context, r *http.Request) (*password
pr := new(passwordResetRequestBody)
err := json.NewDecoder(r.Body).Decode(pr)
if err != nil {
return nil, &platform.Error{
Code: platform.EInvalid,
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
@ -169,11 +167,11 @@ func (h *UserHandler) handlePostUser(w http.ResponseWriter, r *http.Request) {
}
type postUserRequest struct {
User *platform.User
User *influxdb.User
}
func decodePostUserRequest(ctx context.Context, r *http.Request) (*postUserRequest, error) {
b := &platform.User{}
b := &influxdb.User{}
if err := json.NewDecoder(r.Body).Decode(b); err != nil {
return nil, err
}
@ -187,17 +185,17 @@ func decodePostUserRequest(ctx context.Context, r *http.Request) (*postUserReque
func (h *UserHandler) handleGetMe(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
a, err := platcontext.GetAuthorizer(ctx)
a, err := icontext.GetAuthorizer(ctx)
if err != nil {
EncodeError(ctx, err, w)
return
}
var id platform.ID
var id influxdb.ID
switch s := a.(type) {
case *platform.Session:
case *influxdb.Session:
id = s.UserID
case *platform.Authorization:
case *influxdb.Authorization:
id = s.UserID
}
@ -236,20 +234,20 @@ func (h *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
}
type getUserRequest struct {
UserID platform.ID
UserID influxdb.ID
}
func decodeGetUserRequest(ctx context.Context, r *http.Request) (*getUserRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
@ -280,20 +278,20 @@ func (h *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
}
type deleteUserRequest struct {
UserID platform.ID
UserID influxdb.ID
}
func decodeDeleteUserRequest(ctx context.Context, r *http.Request) (*deleteUserRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
@ -308,15 +306,15 @@ type usersResponse struct {
Users []*userResponse `json:"users"`
}
func (us usersResponse) ToPlatform() []*platform.User {
users := make([]*platform.User, len(us.Users))
func (us usersResponse) ToInfluxdb() []*influxdb.User {
users := make([]*influxdb.User, len(us.Users))
for i := range us.Users {
users[i] = &us.Users[i].User
}
return users
}
func newUsersResponse(users []*platform.User) *usersResponse {
func newUsersResponse(users []*influxdb.User) *usersResponse {
res := usersResponse{
Links: map[string]string{
"self": "/api/v2/users",
@ -331,10 +329,10 @@ func newUsersResponse(users []*platform.User) *usersResponse {
type userResponse struct {
Links map[string]string `json:"links"`
platform.User
influxdb.User
}
func newUserResponse(u *platform.User) *userResponse {
func newUserResponse(u *influxdb.User) *userResponse {
return &userResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/users/%s", u.ID),
@ -368,7 +366,7 @@ func (h *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
}
type getUsersRequest struct {
filter platform.UserFilter
filter influxdb.UserFilter
}
func decodeGetUsersRequest(ctx context.Context, r *http.Request) (*getUsersRequest, error) {
@ -376,7 +374,7 @@ func decodeGetUsersRequest(ctx context.Context, r *http.Request) (*getUsersReque
req := &getUsersRequest{}
if userID := qp.Get("id"); userID != "" {
id, err := platform.IDFromString(userID)
id, err := influxdb.IDFromString(userID)
if err != nil {
return nil, err
}
@ -413,26 +411,26 @@ func (h *UserHandler) handlePatchUser(w http.ResponseWriter, r *http.Request) {
}
type patchUserRequest struct {
Update platform.UserUpdate
UserID platform.ID
Update influxdb.UserUpdate
UserID influxdb.ID
}
func decodePatchUserRequest(ctx context.Context, r *http.Request) (*patchUserRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
var upd platform.UserUpdate
var upd influxdb.UserUpdate
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
return nil, err
}
@ -453,7 +451,7 @@ type UserService struct {
}
// FindMe returns user information about the owner of the token
func (s *UserService) FindMe(ctx context.Context, id platform.ID) (*platform.User, error) {
func (s *UserService) FindMe(ctx context.Context, id influxdb.ID) (*influxdb.User, error) {
url, err := newURL(s.Addr, mePath)
if err != nil {
return nil, err
@ -484,7 +482,7 @@ func (s *UserService) FindMe(ctx context.Context, id platform.ID) (*platform.Use
}
// FindUserByID returns a single user by ID.
func (s *UserService) FindUserByID(ctx context.Context, id platform.ID) (*platform.User, error) {
func (s *UserService) FindUserByID(ctx context.Context, id influxdb.ID) (*influxdb.User, error) {
url, err := newURL(s.Addr, userIDPath(id))
if err != nil {
return nil, err
@ -516,19 +514,25 @@ func (s *UserService) FindUserByID(ctx context.Context, id platform.ID) (*platfo
}
// FindUser returns the first user that matches filter.
func (s *UserService) FindUser(ctx context.Context, filter platform.UserFilter) (*platform.User, error) {
func (s *UserService) FindUser(ctx context.Context, filter influxdb.UserFilter) (*influxdb.User, error) {
if filter.ID == nil && filter.Name == nil {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "user not found",
}
}
users, n, err := s.FindUsers(ctx, filter)
if err != nil {
return nil, &platform.Error{
Op: s.OpPrefix + platform.OpFindUser,
return nil, &influxdb.Error{
Op: s.OpPrefix + influxdb.OpFindUser,
Err: err,
}
}
if n == 0 {
return nil, &platform.Error{
Code: platform.ENotFound,
Op: s.OpPrefix + platform.OpFindUser,
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Op: s.OpPrefix + influxdb.OpFindUser,
Msg: "no results found",
}
}
@ -538,7 +542,7 @@ func (s *UserService) FindUser(ctx context.Context, filter platform.UserFilter)
// FindUsers returns a list of users that match filter and the total count of matching users.
// Additional options provide pagination & sorting.
func (s *UserService) FindUsers(ctx context.Context, filter platform.UserFilter, opt ...platform.FindOptions) ([]*platform.User, int, error) {
func (s *UserService) FindUsers(ctx context.Context, filter influxdb.UserFilter, opt ...influxdb.FindOptions) ([]*influxdb.User, int, error) {
url, err := newURL(s.Addr, usersPath)
if err != nil {
return nil, 0, err
@ -576,12 +580,12 @@ func (s *UserService) FindUsers(ctx context.Context, filter platform.UserFilter,
return nil, 0, err
}
us := r.ToPlatform()
us := r.ToInfluxdb()
return us, len(us), nil
}
// CreateUser creates a new user and sets u.ID with the new identifier.
func (s *UserService) CreateUser(ctx context.Context, u *platform.User) error {
func (s *UserService) CreateUser(ctx context.Context, u *influxdb.User) error {
url, err := newURL(s.Addr, usersPath)
if err != nil {
return err
@ -622,7 +626,7 @@ func (s *UserService) CreateUser(ctx context.Context, u *platform.User) error {
// UpdateUser updates a single user with changeset.
// Returns the new user state after update.
func (s *UserService) UpdateUser(ctx context.Context, id platform.ID, upd platform.UserUpdate) (*platform.User, error) {
func (s *UserService) UpdateUser(ctx context.Context, id influxdb.ID, upd influxdb.UserUpdate) (*influxdb.User, error) {
url, err := newURL(s.Addr, userIDPath(id))
if err != nil {
return nil, err
@ -662,7 +666,7 @@ func (s *UserService) UpdateUser(ctx context.Context, id platform.ID, upd platfo
}
// DeleteUser removes a user by ID.
func (s *UserService) DeleteUser(ctx context.Context, id platform.ID) error {
func (s *UserService) DeleteUser(ctx context.Context, id influxdb.ID) error {
url, err := newURL(s.Addr, userIDPath(id))
if err != nil {
return err
@ -684,7 +688,7 @@ func (s *UserService) DeleteUser(ctx context.Context, id platform.ID) error {
return CheckErrorStatus(http.StatusNoContent, resp)
}
func userIDPath(id platform.ID) string {
func userIDPath(id influxdb.ID) string {
return path.Join(usersPath, id.String())
}
@ -711,21 +715,21 @@ func (h *UserHandler) handleGetUserLog(w http.ResponseWriter, r *http.Request) {
}
type getUserLogRequest struct {
UserID platform.ID
opts platform.FindOptions
UserID influxdb.ID
opts influxdb.FindOptions
}
func decodeGetUserLogRequest(ctx context.Context, r *http.Request) (*getUserLogRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
@ -741,7 +745,7 @@ func decodeGetUserLogRequest(ctx context.Context, r *http.Request) (*getUserLogR
}, nil
}
func newUserLogResponse(id platform.ID, es []*platform.OperationLogEntry) *operationLogResponse {
func newUserLogResponse(id influxdb.ID, es []*influxdb.OperationLogEntry) *operationLogResponse {
log := make([]*operationLogEntryResponse, 0, len(es))
for _, e := range es {
log = append(log, newOperationLogEntryResponse(e))

View File

@ -15,12 +15,10 @@ import (
// NewMockUserBackend returns a UserBackend with mock services.
func NewMockUserBackend() *UserBackend {
return &UserBackend{
Logger: zap.NewNop().With(zap.String("handler", "user")),
UserService: mock.NewUserService(),
Logger: zap.NewNop().With(zap.String("handler", "user")),
UserService: mock.NewUserService(),
UserOperationLogService: mock.NewUserOperationLogService(),
BasicAuthService: mock.NewBasicAuthService("", ""),
PasswordsService: mock.NewPasswordsService("", ""),
}
}

View File

@ -1,31 +0,0 @@
package inmem
import (
"context"
"testing"
platform "github.com/influxdata/influxdb"
platformtesting "github.com/influxdata/influxdb/testing"
)
func initBasicAuthService(f platformtesting.UserFields, t *testing.T) (platform.BasicAuthService, func()) {
s := NewService()
s.IDGenerator = f.IDGenerator
ctx := context.Background()
for _, u := range f.Users {
if err := s.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
return s, func() {}
}
func TestBasicAuth(t *testing.T) {
t.Parallel()
platformtesting.BasicAuth(initBasicAuthService, t)
}
func TestBasicAuth_CompareAndSet(t *testing.T) {
t.Parallel()
platformtesting.CompareAndSetPassword(initBasicAuthService, t)
}

View File

@ -27,6 +27,9 @@ func NewKVStore() *KVStore {
func (s *KVStore) View(fn func(kv.Tx) error) error {
s.mu.RLock()
defer s.mu.RUnlock()
if s.buckets == nil {
s.buckets = map[string]*Bucket{}
}
return fn(&Tx{
kv: s,
writable: false,
@ -38,6 +41,10 @@ func (s *KVStore) View(fn func(kv.Tx) error) error {
func (s *KVStore) Update(fn func(kv.Tx) error) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.buckets == nil {
s.buckets = map[string]*Bucket{}
}
return fn(&Tx{
kv: s,
writable: true,
@ -45,6 +52,27 @@ func (s *KVStore) Update(fn func(kv.Tx) error) error {
})
}
// Flush removes all data from the buckets. Used for testing.
func (s *KVStore) Flush() {
s.mu.Lock()
defer s.mu.Unlock()
for _, b := range s.buckets {
b.btree.Clear(false)
}
}
// Buckets returns the names of all buckets within inmem.KVStore.
func (s *KVStore) Buckets() [][]byte {
s.mu.RLock()
defer s.mu.RUnlock()
buckets := make([][]byte, 0, len(s.buckets))
for b := range s.buckets {
buckets = append(buckets, []byte(b))
}
return buckets
}
// Tx is an in memory transaction.
// TODO: make transactions actually transactional
type Tx struct {

View File

@ -1,41 +1,15 @@
package inmem_test
import (
"context"
"reflect"
"sort"
"testing"
platform "github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/inmem"
"github.com/influxdata/influxdb/kv"
platformtesting "github.com/influxdata/influxdb/testing"
)
func initExampleService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) {
s := inmem.NewKVStore()
svc := kv.NewExampleService(s, f.IDGenerator)
if err := svc.Initialize(); err != nil {
t.Fatalf("error initializing user service: %v", err)
}
ctx := context.Background()
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
return svc, "kv/", func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove users: %v", err)
}
}
}
}
func TestExampleService(t *testing.T) {
platformtesting.UserService(initExampleService, t)
}
func initKVStore(f platformtesting.KVStoreFields, t *testing.T) (kv.Store, func()) {
s := inmem.NewKVStore()
@ -62,3 +36,45 @@ func initKVStore(f platformtesting.KVStoreFields, t *testing.T) (kv.Store, func(
func TestKVStore(t *testing.T) {
platformtesting.KVStore(initKVStore, t)
}
func TestKVStore_Buckets(t *testing.T) {
tests := []struct {
name string
buckets []string
want [][]byte
}{
{
name: "single bucket is returned if only one bucket is added",
buckets: []string{"b1"},
want: [][]byte{[]byte("b1")},
},
{
name: "multiple buckets are returned if multiple buckets added",
buckets: []string{"b1", "b2", "b3"},
want: [][]byte{[]byte("b1"), []byte("b2"), []byte("b3")},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &inmem.KVStore{}
err := s.Update(func(tx kv.Tx) error {
for _, b := range tt.buckets {
if _, err := tx.Bucket([]byte(b)); err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatalf("unable to setup store with buckets: %v", err)
}
got := s.Buckets()
sort.Slice(got, func(i, j int) bool {
return string(got[i]) < string(got[j])
})
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("KVStore.Buckets() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -2,19 +2,43 @@ package inmem
import (
"context"
"fmt"
platform "github.com/influxdata/influxdb"
"golang.org/x/crypto/bcrypt"
)
// MinPasswordLength is the shortest password we allow into the system.
const MinPasswordLength = 8
var (
// EIncorrectPassword is returned when any password operation fails in which
// we do not want to leak information.
EIncorrectPassword = &platform.Error{
Msg: "<forbidden> your username or password is incorrect",
}
// EShortPassword is used when a password is less than the minimum
// acceptable password length.
EShortPassword = &platform.Error{
Msg: "<invalid> passwords are required to be longer than 8 characters",
}
)
var _ platform.PasswordsService = (*Service)(nil)
// HashCost is currently using bcrypt defaultCost
const HashCost = bcrypt.DefaultCost
// SetPassword stores the password hash associated with a user.
func (s *Service) SetPassword(ctx context.Context, name string, password string) error {
if len(password) < MinPasswordLength {
return EShortPassword
}
u, err := s.FindUser(ctx, platform.UserFilter{Name: &name})
if err != nil {
return err
return EIncorrectPassword
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), HashCost)
if err != nil {
@ -30,14 +54,17 @@ func (s *Service) SetPassword(ctx context.Context, name string, password string)
func (s *Service) ComparePassword(ctx context.Context, name string, password string) error {
u, err := s.FindUser(ctx, platform.UserFilter{Name: &name})
if err != nil {
return err
return EIncorrectPassword
}
hash, ok := s.basicAuthKV.Load(u.ID.String())
if !ok {
hash = []byte{}
}
return bcrypt.CompareHashAndPassword(hash.([]byte), []byte(password))
if err := bcrypt.CompareHashAndPassword(hash.([]byte), []byte(password)); err != nil {
return fmt.Errorf("<forbidden> your username or password is incorrect")
}
return nil
}
// CompareAndSetPassword replaces the old password with the new password if thee old password is correct.

38
inmem/passwords_test.go Normal file
View File

@ -0,0 +1,38 @@
package inmem
import (
"context"
"testing"
platform "github.com/influxdata/influxdb"
platformtesting "github.com/influxdata/influxdb/testing"
)
func initPasswordsService(f platformtesting.PasswordFields, t *testing.T) (platform.PasswordsService, func()) {
s := NewService()
s.IDGenerator = f.IDGenerator
ctx := context.Background()
for _, u := range f.Users {
if err := s.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
for i := range f.Passwords {
if err := s.SetPassword(ctx, f.Users[i].Name, f.Passwords[i]); err != nil {
t.Fatalf("error setting passsword user, %s %s: %v", f.Users[i].Name, f.Passwords[i], err)
}
}
return s, func() {}
}
func TestPasswords(t *testing.T) {
t.Parallel()
platformtesting.PasswordsService(initPasswordsService, t)
}
func TestPasswords_CompareAndSet(t *testing.T) {
t.Parallel()
platformtesting.CompareAndSetPassword(initPasswordsService, t)
}

View File

@ -56,14 +56,14 @@ func (s *Service) AddTarget(ctx context.Context, target *platform.ScraperTarget,
if !target.OrgID.Valid() {
return &platform.Error{
Code: platform.EInvalid,
Msg: "org id is invalid",
Msg: "provided organization ID has invalid format",
Op: OpPrefix + platform.OpAddTarget,
}
}
if !target.BucketID.Valid() {
return &platform.Error{
Code: platform.EInvalid,
Msg: "bucket id is invalid",
Msg: "provided bucket ID has invalid format",
Op: OpPrefix + platform.OpAddTarget,
}
}
@ -116,7 +116,7 @@ func (s *Service) UpdateTarget(ctx context.Context, update *platform.ScraperTarg
return nil, &platform.Error{
Code: platform.EInvalid,
Op: op,
Msg: "id is invalid",
Msg: "provided scraper target ID has invalid format",
}
}
oldTarget, pe := s.loadScraperTarget(update.ID)

View File

@ -1,6 +1,7 @@
package inmem
import (
"context"
"sync"
"time"
@ -29,6 +30,8 @@ type Service struct {
telegrafConfigKV sync.Map
onboardingKV sync.Map
basicAuthKV sync.Map
sessionKV sync.Map
sourceKV sync.Map
TokenGenerator platform.TokenGenerator
IDGenerator platform.IDGenerator
@ -37,11 +40,13 @@ type Service struct {
// NewService creates an instance of a Service.
func NewService() *Service {
return &Service{
s := &Service{
TokenGenerator: rand.NewTokenGenerator(64),
IDGenerator: snowflake.NewIDGenerator(),
time: time.Now,
}
s.initializeSources(context.TODO())
return s
}
// WithTime sets the function for computing the current time. Used for updating meta data
@ -49,3 +54,39 @@ func NewService() *Service {
func (s *Service) WithTime(fn func() time.Time) {
s.time = fn
}
// Flush removes all data from the in-memory store
func (s *Service) Flush() {
s.flush(&s.authorizationKV)
s.flush(&s.organizationKV)
s.flush(&s.bucketKV)
s.flush(&s.userKV)
s.flush(&s.dashboardKV)
s.flush(&s.viewKV)
s.flush(&s.variableKV)
s.flush(&s.dbrpMappingKV)
s.flush(&s.userResourceMappingKV)
s.flush(&s.labelKV)
s.flush(&s.labelMappingKV)
s.flush(&s.scraperTargetKV)
s.flush(&s.telegrafConfigKV)
s.flush(&s.onboardingKV)
s.flush(&s.basicAuthKV)
s.flush(&s.sessionKV)
s.flush(&s.sourceKV)
}
func (s *Service) flush(m *sync.Map) {
keys := []interface{}{}
f := func(key, value interface{}) bool {
keys = append(keys, key)
return true
}
m.Range(f)
for _, k := range keys {
m.Delete(k)
}
}

99
inmem/session.go Normal file
View File

@ -0,0 +1,99 @@
package inmem
import (
"context"
"time"
platform "github.com/influxdata/influxdb"
)
// RenewSession extends the expire time to newExpiration.
func (s *Service) RenewSession(ctx context.Context, session *platform.Session, newExpiration time.Time) error {
if session == nil {
return &platform.Error{
Msg: "session is nil",
}
}
session.ExpiresAt = newExpiration
return s.PutSession(ctx, session)
}
// FindSession retrieves the session found at the provided key.
func (s *Service) FindSession(ctx context.Context, key string) (*platform.Session, error) {
result, found := s.sessionKV.Load(key)
if !found {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: platform.ErrSessionNotFound,
}
}
sess := new(platform.Session)
*sess = result.(platform.Session)
// TODO(desa): these values should be cached so it's not so expensive to lookup each time.
f := platform.UserResourceMappingFilter{UserID: sess.UserID}
mappings, _, err := s.FindUserResourceMappings(ctx, f)
if err != nil {
return nil, &platform.Error{
Err: err,
}
}
ps := make([]platform.Permission, 0, len(mappings))
for _, m := range mappings {
p, err := m.ToPermissions()
if err != nil {
return nil, &platform.Error{
Err: err,
}
}
ps = append(ps, p...)
}
ps = append(ps, platform.MePermissions(sess.UserID)...)
sess.Permissions = ps
return sess, nil
}
func (s *Service) PutSession(ctx context.Context, sess *platform.Session) error {
s.sessionKV.Store(sess.Key, *sess)
return nil
}
// ExpireSession expires the session at the provided key.
func (s *Service) ExpireSession(ctx context.Context, key string) error {
return nil
}
// CreateSession creates a session for a user with the users maximal privileges.
func (s *Service) CreateSession(ctx context.Context, user string) (*platform.Session, error) {
u, pe := s.findUserByName(ctx, user)
if pe != nil {
return nil, &platform.Error{
Err: pe,
}
}
sess := &platform.Session{}
sess.ID = s.IDGenerator.ID()
k, err := s.TokenGenerator.Token()
if err != nil {
return nil, &platform.Error{
Err: err,
}
}
sess.Key = k
sess.UserID = u.ID
sess.CreatedAt = time.Now()
// TODO(desa): make this configurable
sess.ExpiresAt = sess.CreatedAt.Add(time.Hour)
// TODO(desa): not totally sure what to do here. Possibly we should have a maximal privilege permission.
sess.Permissions = []platform.Permission{}
if err := s.PutSession(ctx, sess); err != nil {
return nil, err
}
return sess, nil
}

147
inmem/source.go Normal file
View File

@ -0,0 +1,147 @@
package inmem
import (
"context"
"fmt"
platform "github.com/influxdata/influxdb"
)
// DefaultSource is the default source.
var DefaultSource = platform.Source{
Default: true,
Name: "autogen",
Type: platform.SelfSourceType,
}
const (
// DefaultSourceID it the default source identifier
DefaultSourceID = "020f755c3c082000"
// DefaultSourceOrganizationID is the default source's organization identifier
DefaultSourceOrganizationID = "50616e67652c206c"
)
func init() {
if err := DefaultSource.ID.DecodeFromString(DefaultSourceID); err != nil {
panic(fmt.Sprintf("failed to decode default source id: %v", err))
}
if err := DefaultSource.OrganizationID.DecodeFromString(DefaultSourceOrganizationID); err != nil {
panic(fmt.Sprintf("failed to decode default source organization id: %v", err))
}
}
func (s *Service) initializeSources(ctx context.Context) error {
_, pe := s.FindSourceByID(ctx, DefaultSource.ID)
if pe != nil && platform.ErrorCode(pe) != platform.ENotFound {
return pe
}
if platform.ErrorCode(pe) == platform.ENotFound {
if err := s.PutSource(ctx, &DefaultSource); err != nil {
return err
}
}
return nil
}
// DefaultSource retrieves the default source.
func (s *Service) DefaultSource(ctx context.Context) (*platform.Source, error) {
// TODO(desa): make this faster by putting the default source in an index.
srcs, _, err := s.FindSources(ctx, platform.FindOptions{})
if err != nil {
return nil, err
}
for _, src := range srcs {
if src.Default {
return src, nil
}
}
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "no default source found",
}
}
// FindSourceByID retrieves a source by id.
func (s *Service) FindSourceByID(ctx context.Context, id platform.ID) (*platform.Source, error) {
i, ok := s.sourceKV.Load(id.String())
if !ok {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: platform.ErrSourceNotFound,
}
}
src, ok := i.(*platform.Source)
if !ok {
return nil, &platform.Error{
Code: platform.EInvalid,
Msg: fmt.Sprintf("type %T is not a source", i),
}
}
return src, nil
}
// FindSources retrives all sources that match an arbitrary source filter.
// Filters using ID, or OrganizationID and source Name should be efficient.
// Other filters will do a linear scan across all sources searching for a match.
func (s *Service) FindSources(ctx context.Context, opt platform.FindOptions) ([]*platform.Source, int, error) {
var ds []*platform.Source
s.sourceKV.Range(func(k, v interface{}) bool {
d, ok := v.(*platform.Source)
if !ok {
return false
}
ds = append(ds, d)
return true
})
return ds, len(ds), nil
}
// CreateSource creates a platform source and sets s.ID.
func (s *Service) CreateSource(ctx context.Context, src *platform.Source) error {
src.ID = s.IDGenerator.ID()
if err := s.PutSource(ctx, src); err != nil {
return &platform.Error{
Err: err,
}
}
return nil
}
// PutSource will put a source without setting an ID.
func (s *Service) PutSource(ctx context.Context, src *platform.Source) error {
s.sourceKV.Store(src.ID.String(), src)
return nil
}
// UpdateSource updates a source according the parameters set on upd.
func (s *Service) UpdateSource(ctx context.Context, id platform.ID, upd platform.SourceUpdate) (*platform.Source, error) {
src, err := s.FindSourceByID(ctx, id)
if err != nil {
return nil, &platform.Error{
Err: err,
Op: OpPrefix + platform.OpUpdateView,
}
}
upd.Apply(src)
s.sourceKV.Store(src.ID.String(), src)
return src, nil
}
// DeleteSource deletes a source and prunes it from the index.
func (s *Service) DeleteSource(ctx context.Context, id platform.ID) error {
if _, err := s.FindSourceByID(ctx, id); err != nil {
return &platform.Error{
Err: err,
Op: OpPrefix + platform.OpDeleteView,
}
}
s.sourceKV.Delete(id.String())
return nil
}

View File

@ -10,11 +10,9 @@ var _ platform.TelegrafConfigStore = new(Service)
// FindTelegrafConfigByID returns a single telegraf config by ID.
func (s *Service) FindTelegrafConfigByID(ctx context.Context, id platform.ID) (tc *platform.TelegrafConfig, err error) {
op := OpPrefix + platform.OpFindTelegrafConfigByID
var pErr *platform.Error
tc, pErr = s.findTelegrafConfigByID(ctx, id)
if pErr != nil {
pErr.Op = op
err = pErr
}
return tc, err
@ -23,8 +21,8 @@ func (s *Service) FindTelegrafConfigByID(ctx context.Context, id platform.ID) (t
func (s *Service) findTelegrafConfigByID(ctx context.Context, id platform.ID) (*platform.TelegrafConfig, *platform.Error) {
if !id.Valid() {
return nil, &platform.Error{
Code: platform.EEmptyValue,
Err: platform.ErrInvalidID,
Code: platform.EInvalid,
Msg: "provided telegraf configuration ID has invalid format",
}
}
result, found := s.telegrafConfigKV.Load(id)
@ -39,22 +37,6 @@ func (s *Service) findTelegrafConfigByID(ctx context.Context, id platform.ID) (*
return tc, nil
}
// FindTelegrafConfig returns the first telegraf config that matches filter.
func (s *Service) FindTelegrafConfig(ctx context.Context, filter platform.TelegrafConfigFilter) (*platform.TelegrafConfig, error) {
op := OpPrefix + platform.OpFindTelegrafConfig
tcs, n, err := s.FindTelegrafConfigs(ctx, filter, platform.FindOptions{Limit: 1})
if err != nil {
return nil, err
}
if n > 0 {
return tcs[0], nil
}
return nil, &platform.Error{
Code: platform.ENotFound,
Op: op,
}
}
func (s *Service) findTelegrafConfigs(ctx context.Context, filter platform.TelegrafConfigFilter, opt ...platform.FindOptions) ([]*platform.TelegrafConfig, int, *platform.Error) {
tcs := make([]*platform.TelegrafConfig, 0)
m, _, err := s.FindUserResourceMappings(ctx, filter.UserResourceMappingFilter)
@ -169,9 +151,8 @@ func (s *Service) DeleteTelegrafConfig(ctx context.Context, id platform.ID) erro
var err error
if !id.Valid() {
return &platform.Error{
Op: op,
Code: platform.EEmptyValue,
Err: platform.ErrInvalidID,
Msg: "provided telegraf configuration ID has invalid format",
Code: platform.EInvalid,
}
}
if _, pErr := s.findTelegrafConfigByID(ctx, id); pErr != nil {

View File

@ -15,7 +15,10 @@ func encodeUserResourceMappingKey(resourceID, userID platform.ID) string {
func (s *Service) loadUserResourceMapping(ctx context.Context, resourceID, userID platform.ID) (*platform.UserResourceMapping, error) {
i, ok := s.userResourceMappingKV.Load(encodeUserResourceMappingKey(resourceID, userID))
if !ok {
return nil, fmt.Errorf("userResource mapping not found")
return nil, &platform.Error{
Msg: "user to resource mapping not found",
Code: platform.ENotFound,
}
}
m, ok := i.(platform.UserResourceMapping)
@ -87,7 +90,10 @@ func (s *Service) FindUserResourceMappings(ctx context.Context, filter platform.
func (s *Service) CreateUserResourceMapping(ctx context.Context, m *platform.UserResourceMapping) error {
mapping, _ := s.FindUserResourceBy(ctx, m.ResourceID, m.UserID)
if mapping != nil {
return fmt.Errorf("mapping for user %s already exists", m.UserID)
return &platform.Error{
Code: platform.EInternal,
Msg: fmt.Sprintf("Unexpected error when assigning user to a resource: mapping for user %s already exists", m.UserID),
}
}
s.userResourceMappingKV.Store(encodeUserResourceMappingKey(m.ResourceID, m.UserID), *m)

View File

@ -4,23 +4,35 @@ import (
"context"
"testing"
platform "github.com/influxdata/influxdb"
platformtesting "github.com/influxdata/influxdb/testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func initUserService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) {
s := NewService()
s.IDGenerator = f.IDGenerator
func initUserService(f influxdbtesting.UserFields, t *testing.T) (influxdb.UserService, string, func()) {
s := NewKVStore()
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing user service: %v", err)
}
for _, u := range f.Users {
if err := s.PutUser(ctx, u); err != nil {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
return s, OpPrefix, func() {}
return svc, "kv/", func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove users: %v", err)
}
}
}
}
func TestUserService(t *testing.T) {
t.Parallel()
platformtesting.UserService(initUserService, t)
influxdbtesting.UserService(initUserService, t)
}

View File

@ -5,7 +5,7 @@ import (
"time"
)
// KeyValuleLog is a generic type logs key-value pairs. This interface is intended to be used to construct other
// KeyValueLog is a generic type logs key-value pairs. This interface is intended to be used to construct other
// higher-level log-like resources such as an oplog or audit log.
//
// The idea is to create a log who values can be accessed at the key k:

485
kv/auth.go Normal file
View File

@ -0,0 +1,485 @@
package kv
import (
"context"
"encoding/json"
"fmt"
influxdb "github.com/influxdata/influxdb"
)
var (
authBucket = []byte("authorizationsv1")
authIndex = []byte("authorizationindexv1")
)
var _ influxdb.AuthorizationService = (*Service)(nil)
func (s *Service) initializeAuths(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket(authBucket); err != nil {
return err
}
if _, err := authIndexBucket(tx); err != nil {
return err
}
return nil
}
// FindAuthorizationByID retrieves a authorization by id.
func (s *Service) FindAuthorizationByID(ctx context.Context, id influxdb.ID) (*influxdb.Authorization, error) {
var a *influxdb.Authorization
err := s.kv.View(func(tx Tx) error {
auth, err := s.findAuthorizationByID(ctx, tx, id)
if err != nil {
return err
}
a = auth
return nil
})
if err != nil {
return nil, err
}
return a, nil
}
func (s *Service) findAuthorizationByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Authorization, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
b, err := tx.Bucket(authBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "authorization not found",
}
}
if err != nil {
return nil, err
}
a := &influxdb.Authorization{}
if err := decodeAuthorization(v, a); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
return a, nil
}
// FindAuthorizationByToken returns a authorization by token for a particular authorization.
func (s *Service) FindAuthorizationByToken(ctx context.Context, n string) (*influxdb.Authorization, error) {
var a *influxdb.Authorization
err := s.kv.View(func(tx Tx) error {
auth, err := s.findAuthorizationByToken(ctx, tx, n)
if err != nil {
return err
}
a = auth
return nil
})
if err != nil {
return nil, err
}
return a, nil
}
func (s *Service) findAuthorizationByToken(ctx context.Context, tx Tx, n string) (*influxdb.Authorization, error) {
idx, err := authIndexBucket(tx)
if err != nil {
return nil, err
}
a, err := idx.Get(authIndexKey(n))
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "authorization not found",
}
}
var id influxdb.ID
if err := id.Decode(a); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
return s.findAuthorizationByID(ctx, tx, id)
}
func filterAuthorizationsFn(filter influxdb.AuthorizationFilter) func(a *influxdb.Authorization) bool {
if filter.ID != nil {
return func(a *influxdb.Authorization) bool {
return a.ID == *filter.ID
}
}
if filter.Token != nil {
return func(a *influxdb.Authorization) bool {
return a.Token == *filter.Token
}
}
if filter.UserID != nil {
return func(a *influxdb.Authorization) bool {
return a.UserID == *filter.UserID
}
}
return func(a *influxdb.Authorization) bool { return true }
}
// FindAuthorizations retrives all authorizations that match an arbitrary authorization filter.
// Filters using ID, or Token should be efficient.
// Other filters will do a linear scan across all authorizations searching for a match.
func (s *Service) FindAuthorizations(ctx context.Context, filter influxdb.AuthorizationFilter, opt ...influxdb.FindOptions) ([]*influxdb.Authorization, int, error) {
if filter.ID != nil {
a, err := s.FindAuthorizationByID(ctx, *filter.ID)
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return []*influxdb.Authorization{a}, 1, nil
}
if filter.Token != nil {
a, err := s.FindAuthorizationByToken(ctx, *filter.Token)
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return []*influxdb.Authorization{a}, 1, nil
}
as := []*influxdb.Authorization{}
err := s.kv.View(func(tx Tx) error {
auths, err := s.findAuthorizations(ctx, tx, filter)
if err != nil {
return err
}
as = auths
return nil
})
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return as, len(as), nil
}
func (s *Service) findAuthorizations(ctx context.Context, tx Tx, f influxdb.AuthorizationFilter) ([]*influxdb.Authorization, error) {
// If the users name was provided, look up user by ID first
if f.User != nil {
u, err := s.findUserByName(ctx, tx, *f.User)
if err != nil {
return nil, err
}
f.UserID = &u.ID
}
as := []*influxdb.Authorization{}
filterFn := filterAuthorizationsFn(f)
err := s.forEachAuthorization(ctx, tx, func(a *influxdb.Authorization) bool {
if filterFn(a) {
as = append(as, a)
}
return true
})
if err != nil {
return nil, err
}
return as, nil
}
// CreateAuthorization creates a influxdb authorization and sets b.ID, and b.UserID if not provided.
func (s *Service) CreateAuthorization(ctx context.Context, a *influxdb.Authorization) error {
return s.kv.Update(func(tx Tx) error {
return s.createAuthorization(ctx, tx, a)
})
}
func (s *Service) createAuthorization(ctx context.Context, tx Tx, a *influxdb.Authorization) error {
if err := a.Valid(); err != nil {
return &influxdb.Error{
Err: err,
}
}
if _, err := s.findUserByID(ctx, tx, a.UserID); err != nil {
return influxdb.ErrUnableToCreateToken
}
if _, err := s.findOrganizationByID(ctx, tx, a.OrgID); err != nil {
return influxdb.ErrUnableToCreateToken
}
if err := s.uniqueAuthToken(ctx, tx, a); err != nil {
return err
}
if a.Token == "" {
token, err := s.TokenGenerator.Token()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
a.Token = token
}
a.ID = s.IDGenerator.ID()
if err := s.putAuthorization(ctx, tx, a); err != nil {
return err
}
return nil
}
// PutAuthorization will put a authorization without setting an ID.
func (s *Service) PutAuthorization(ctx context.Context, a *influxdb.Authorization) error {
return s.kv.Update(func(tx Tx) error {
return s.putAuthorization(ctx, tx, a)
})
}
func encodeAuthorization(a *influxdb.Authorization) ([]byte, error) {
switch a.Status {
case influxdb.Active, influxdb.Inactive:
case "":
a.Status = influxdb.Active
default:
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "unknown authorization status",
}
}
return json.Marshal(a)
}
func (s *Service) putAuthorization(ctx context.Context, tx Tx, a *influxdb.Authorization) error {
v, err := encodeAuthorization(a)
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
encodedID, err := a.ID.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.ENotFound,
Err: err,
}
}
idx, err := authIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Put(authIndexKey(a.Token), encodedID); err != nil {
return &influxdb.Error{
Code: influxdb.EInternal,
Err: err,
}
}
b, err := tx.Bucket(authBucket)
if err != nil {
return err
}
if err := b.Put(encodedID, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func authIndexKey(n string) []byte {
return []byte(n)
}
func decodeAuthorization(b []byte, a *influxdb.Authorization) error {
if err := json.Unmarshal(b, a); err != nil {
return err
}
if a.Status == "" {
a.Status = influxdb.Active
}
return nil
}
// forEachAuthorization will iterate through all authorizations while fn returns true.
func (s *Service) forEachAuthorization(ctx context.Context, tx Tx, fn func(*influxdb.Authorization) bool) error {
b, err := tx.Bucket(authBucket)
if err != nil {
return err
}
cur, err := b.Cursor()
if err != nil {
return err
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
a := &influxdb.Authorization{}
if err := decodeAuthorization(v, a); err != nil {
return err
}
if !fn(a) {
break
}
}
return nil
}
// DeleteAuthorization deletes a authorization and prunes it from the index.
func (s *Service) DeleteAuthorization(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(func(tx Tx) (err error) {
return s.deleteAuthorization(ctx, tx, id)
})
}
func (s *Service) deleteAuthorization(ctx context.Context, tx Tx, id influxdb.ID) error {
a, err := s.findAuthorizationByID(ctx, tx, id)
if err != nil {
return err
}
idx, err := authIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Delete(authIndexKey(a.Token)); err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(authBucket)
if err != nil {
return err
}
if err := b.Delete(encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// SetAuthorizationStatus updates the status of the authorization. Useful
// for setting an authorization to inactive or active.
func (s *Service) SetAuthorizationStatus(ctx context.Context, id influxdb.ID, status influxdb.Status) error {
return s.kv.Update(func(tx Tx) error {
return s.updateAuthorization(ctx, tx, id, status)
})
}
func (s *Service) updateAuthorization(ctx context.Context, tx Tx, id influxdb.ID, status influxdb.Status) error {
a, err := s.findAuthorizationByID(ctx, tx, id)
if err != nil {
return err
}
a.Status = status
v, err := encodeAuthorization(a)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(authBucket)
if err != nil {
return err
}
if err = b.Put(encodedID, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func authIndexBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket([]byte(authIndex))
if err != nil {
return nil, UnexpectedAuthIndexError(err)
}
return b, nil
}
// UnexpectedAuthIndexError is used when the error comes from an internal system.
func UnexpectedAuthIndexError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving auth index; Err: %v", err),
Op: "kv/authIndex",
}
}
func (s *Service) uniqueAuthToken(ctx context.Context, tx Tx, a *influxdb.Authorization) error {
err := s.unique(ctx, tx, authIndex, authIndexKey(a.Token))
if err == NotUniqueError {
// by returning a generic error we are trying to hide when
// a token is non-unique.
return influxdb.ErrUnableToCreateToken
}
// otherwise, this is some sort of internal server error and we
// should provide some debugging information.
return err
}

93
kv/auth_test.go Normal file
View File

@ -0,0 +1,93 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltAuthorizationService(t *testing.T) {
influxdbtesting.AuthorizationService(initBoltAuthorizationService, t)
}
func TestInmemAuthorizationService(t *testing.T) {
influxdbtesting.AuthorizationService(initInmemAuthorizationService, t)
}
func initBoltAuthorizationService(f influxdbtesting.AuthorizationFields, t *testing.T) (influxdb.AuthorizationService, string, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initAuthorizationService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initInmemAuthorizationService(f influxdbtesting.AuthorizationFields, t *testing.T) (influxdb.AuthorizationService, string, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initAuthorizationService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initAuthorizationService(s kv.Store, f influxdbtesting.AuthorizationFields, t *testing.T) (influxdb.AuthorizationService, string, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
svc.TokenGenerator = f.TokenGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing authorization service: %v", err)
}
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
for _, o := range f.Orgs {
if err := svc.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate orgs")
}
}
for _, a := range f.Authorizations {
if err := svc.PutAuthorization(ctx, a); err != nil {
t.Fatalf("failed to populate authorizations %s", err)
}
}
return svc, kv.OpPrefix, func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove user: %v", err)
}
}
for _, o := range f.Orgs {
if err := svc.DeleteOrganization(ctx, o.ID); err != nil {
t.Logf("failed to remove org: %v", err)
}
}
for _, a := range f.Authorizations {
if err := svc.DeleteAuthorization(ctx, a.ID); err != nil {
t.Logf("failed to remove authorizations: %v", err)
}
}
}
}

767
kv/bucket.go Normal file
View File

@ -0,0 +1,767 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/influxdata/influxdb"
icontext "github.com/influxdata/influxdb/context"
)
var (
bucketBucket = []byte("bucketsv1")
bucketIndex = []byte("bucketindexv1")
)
var _ influxdb.BucketService = (*Service)(nil)
var _ influxdb.BucketOperationLogService = (*Service)(nil)
func (s *Service) initializeBuckets(ctx context.Context, tx Tx) error {
if _, err := s.bucketsBucket(tx); err != nil {
return err
}
if _, err := s.bucketsIndexBucket(tx); err != nil {
return err
}
return nil
}
func (s *Service) bucketsBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket(bucketBucket)
if err != nil {
return nil, UnexpectedBucketError(err)
}
return b, nil
}
func (s *Service) bucketsIndexBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket(bucketIndex)
if err != nil {
return nil, UnexpectedBucketIndexError(err)
}
return b, nil
}
func (s *Service) setOrganizationOnBucket(ctx context.Context, tx Tx, b *influxdb.Bucket) error {
o, err := s.findOrganizationByID(ctx, tx, b.OrganizationID)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
b.Organization = o.Name
return nil
}
// FindBucketByID retrieves a bucket by id.
func (s *Service) FindBucketByID(ctx context.Context, id influxdb.ID) (*influxdb.Bucket, error) {
var b *influxdb.Bucket
var err error
err = s.kv.View(func(tx Tx) error {
bkt, pe := s.findBucketByID(ctx, tx, id)
if pe != nil {
err = pe
return err
}
b = bkt
return nil
})
if err != nil {
return nil, err
}
return b, nil
}
func (s *Service) findBucketByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Bucket, error) {
var b influxdb.Bucket
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
bkt, err := s.bucketsBucket(tx)
if err != nil {
return nil, err
}
v, err := bkt.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "bucket not found",
}
}
if err != nil {
return nil, err
}
if err := json.Unmarshal(v, &b); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
if err := s.setOrganizationOnBucket(ctx, tx, &b); err != nil {
return nil, &influxdb.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 (s *Service) FindBucketByName(ctx context.Context, orgID influxdb.ID, n string) (*influxdb.Bucket, error) {
var b *influxdb.Bucket
var err error
err = s.kv.View(func(tx Tx) error {
bkt, pe := s.findBucketByName(ctx, tx, orgID, n)
if pe != nil {
err = pe
return err
}
b = bkt
return nil
})
return b, err
}
func (s *Service) findBucketByName(ctx context.Context, tx Tx, orgID influxdb.ID, n string) (*influxdb.Bucket, error) {
b := &influxdb.Bucket{
OrganizationID: orgID,
Name: n,
}
key, err := bucketIndexKey(b)
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
idx, err := s.bucketsIndexBucket(tx)
if err != nil {
return nil, err
}
buf, err := idx.Get(key)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "bucket not found",
}
}
if err != nil {
return nil, err
}
var id influxdb.ID
if err := id.Decode(buf); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return s.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 (s *Service) FindBucket(ctx context.Context, filter influxdb.BucketFilter) (*influxdb.Bucket, error) {
var b *influxdb.Bucket
var err error
if filter.ID != nil {
b, err = s.FindBucketByID(ctx, *filter.ID)
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return b, nil
}
if filter.Name != nil && filter.OrganizationID != nil {
return s.FindBucketByName(ctx, *filter.OrganizationID, *filter.Name)
}
err = s.kv.View(func(tx Tx) error {
if filter.Organization != nil {
o, err := s.findOrganizationByName(ctx, tx, *filter.Organization)
if err != nil {
return err
}
filter.OrganizationID = &o.ID
}
filterFn := filterBucketsFn(filter)
return s.forEachBucket(ctx, tx, false, func(bkt *influxdb.Bucket) bool {
if filterFn(bkt) {
b = bkt
return false
}
return true
})
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
if b == nil {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "bucket not found",
}
}
return b, nil
}
func filterBucketsFn(filter influxdb.BucketFilter) func(b *influxdb.Bucket) bool {
if filter.ID != nil {
return func(b *influxdb.Bucket) bool {
return b.ID == *filter.ID
}
}
if filter.Name != nil && filter.OrganizationID != nil {
return func(b *influxdb.Bucket) bool {
return b.Name == *filter.Name && b.OrganizationID == *filter.OrganizationID
}
}
if filter.Name != nil {
return func(b *influxdb.Bucket) bool {
return b.Name == *filter.Name
}
}
if filter.OrganizationID != nil {
return func(b *influxdb.Bucket) bool {
return b.OrganizationID == *filter.OrganizationID
}
}
return func(b *influxdb.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 (s *Service) FindBuckets(ctx context.Context, filter influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) {
if filter.ID != nil {
b, err := s.FindBucketByID(ctx, *filter.ID)
if err != nil {
return nil, 0, err
}
return []*influxdb.Bucket{b}, 1, nil
}
if filter.Name != nil && filter.OrganizationID != nil {
b, err := s.FindBucketByName(ctx, *filter.OrganizationID, *filter.Name)
if err != nil {
return nil, 0, err
}
return []*influxdb.Bucket{b}, 1, nil
}
bs := []*influxdb.Bucket{}
err := s.kv.View(func(tx Tx) error {
bkts, err := s.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 (s *Service) findBuckets(ctx context.Context, tx Tx, filter influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, error) {
bs := []*influxdb.Bucket{}
if filter.Organization != nil {
o, err := s.findOrganizationByName(ctx, tx, *filter.Organization)
if err != nil {
return nil, &influxdb.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 := s.forEachBucket(ctx, tx, descending, func(b *influxdb.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, &influxdb.Error{
Err: err,
}
}
return bs, nil
}
// CreateBucket creates a influxdb bucket and sets b.ID.
func (s *Service) CreateBucket(ctx context.Context, b *influxdb.Bucket) error {
return s.kv.Update(func(tx Tx) error {
return s.createBucket(ctx, tx, b)
})
}
func (s *Service) createBucket(ctx context.Context, tx Tx, b *influxdb.Bucket) error {
if b.OrganizationID.Valid() {
_, pe := s.findOrganizationByID(ctx, tx, b.OrganizationID)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
} else {
o, pe := s.findOrganizationByName(ctx, tx, b.Organization)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
b.OrganizationID = o.ID
}
// if the bucket name is not unique for this organization, then, do not
// allow creation.
if err := s.uniqueBucketName(ctx, tx, b); err != nil {
return err
}
b.ID = s.IDGenerator.ID()
if err := s.appendBucketEventToLog(ctx, tx, b.ID, bucketCreatedEvent); err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := s.putBucket(ctx, tx, b); err != nil {
return err
}
if err := s.createBucketUserResourceMappings(ctx, tx, b); err != nil {
return err
}
return nil
}
// PutBucket will put a bucket without setting an ID.
func (s *Service) PutBucket(ctx context.Context, b *influxdb.Bucket) error {
return s.kv.Update(func(tx Tx) error {
var err error
pe := s.putBucket(ctx, tx, b)
if pe != nil {
err = pe
}
return err
})
}
func (s *Service) createBucketUserResourceMappings(ctx context.Context, tx Tx, b *influxdb.Bucket) error {
ms, err := s.findUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceType: influxdb.OrgsResourceType,
ResourceID: b.OrganizationID,
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
for _, m := range ms {
if err := s.createUserResourceMapping(ctx, tx, &influxdb.UserResourceMapping{
ResourceType: influxdb.BucketsResourceType,
ResourceID: b.ID,
UserID: m.UserID,
UserType: m.UserType,
}); err != nil {
return &influxdb.Error{
Err: err,
}
}
}
return nil
}
func (s *Service) putBucket(ctx context.Context, tx Tx, b *influxdb.Bucket) error {
b.Organization = ""
v, err := json.Marshal(b)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := b.ID.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
key, pe := bucketIndexKey(b)
if err != nil {
return pe
}
idx, err := s.bucketsIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Put(key, encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
bkt, err := s.bucketsBucket(tx)
if bkt.Put(encodedID, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return s.setOrganizationOnBucket(ctx, tx, b)
}
// bucketIndexKey is a combination of the orgID and the bucket name.
func bucketIndexKey(b *influxdb.Bucket) ([]byte, error) {
orgID, err := b.OrganizationID.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
k := make([]byte, influxdb.IDLength+len(b.Name))
copy(k, orgID)
copy(k[influxdb.IDLength:], []byte(b.Name))
return k, nil
}
// forEachBucket will iterate through all buckets while fn returns true.
func (s *Service) forEachBucket(ctx context.Context, tx Tx, descending bool, fn func(*influxdb.Bucket) bool) error {
bkt, err := s.bucketsBucket(tx)
if err != nil {
return err
}
cur, err := bkt.Cursor()
if err != nil {
return err
}
var k, v []byte
if descending {
k, v = cur.Last()
} else {
k, v = cur.First()
}
for k != nil {
b := &influxdb.Bucket{}
if err := json.Unmarshal(v, b); err != nil {
return err
}
if err := s.setOrganizationOnBucket(ctx, tx, b); err != nil {
return err
}
if !fn(b) {
break
}
if descending {
k, v = cur.Prev()
} else {
k, v = cur.Next()
}
}
return nil
}
func (s *Service) uniqueBucketName(ctx context.Context, tx Tx, b *influxdb.Bucket) error {
key, err := bucketIndexKey(b)
if err != nil {
return err
}
// if the bucket name is not unique for this organization, then, do not
// allow creation.
err = s.unique(ctx, tx, bucketIndex, key)
if err == NotUniqueError {
return BucketAlreadyExistsError(b)
}
return err
}
// UpdateBucket updates a bucket according the parameters set on upd.
func (s *Service) UpdateBucket(ctx context.Context, id influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) {
var b *influxdb.Bucket
err := s.kv.Update(func(tx Tx) error {
bkt, err := s.updateBucket(ctx, tx, id, upd)
if err != nil {
return err
}
b = bkt
return nil
})
return b, err
}
func (s *Service) updateBucket(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) {
b, err := s.findBucketByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.RetentionPeriod != nil {
b.RetentionPeriod = *upd.RetentionPeriod
}
if upd.Name != nil {
key, err := bucketIndexKey(b)
if err != nil {
return nil, err
}
idx, err := s.bucketsIndexBucket(tx)
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 := idx.Delete(key); err != nil {
return nil, err
}
b.Name = *upd.Name
}
if err := s.appendBucketEventToLog(ctx, tx, b.ID, bucketUpdatedEvent); err != nil {
return nil, err
}
if err := s.putBucket(ctx, tx, b); err != nil {
return nil, err
}
if err := s.setOrganizationOnBucket(ctx, tx, b); err != nil {
return nil, err
}
return b, nil
}
// DeleteBucket deletes a bucket and prunes it from the index.
func (s *Service) DeleteBucket(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
var err error
if pe := s.deleteBucket(ctx, tx, id); pe != nil {
err = pe
}
return err
})
}
func (s *Service) deleteBucket(ctx context.Context, tx Tx, id influxdb.ID) error {
b, pe := s.findBucketByID(ctx, tx, id)
if pe != nil {
return pe
}
key, pe := bucketIndexKey(b)
if pe != nil {
return pe
}
idx, err := s.bucketsIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Delete(key); err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
bkt, err := s.bucketsBucket(tx)
if err != nil {
return err
}
if err := bkt.Delete(encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := s.deleteUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: id,
ResourceType: influxdb.BucketsResourceType,
}); err != nil {
return err
}
return nil
}
const bucketOperationLogKeyPrefix = "bucket"
func encodeBucketOperationLogKey(id influxdb.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 (s *Service) GetBucketOperationLog(ctx context.Context, id influxdb.ID, opts influxdb.FindOptions) ([]*influxdb.OperationLogEntry, int, error) {
// TODO(desa): might be worthwhile to allocate a slice of size opts.Limit
log := []*influxdb.OperationLogEntry{}
err := s.kv.View(func(tx Tx) error {
key, err := encodeBucketOperationLogKey(id)
if err != nil {
return err
}
return s.forEachLogEntry(ctx, tx, key, opts, func(v []byte, t time.Time) error {
e := &influxdb.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 (s *Service) appendBucketEventToLog(ctx context.Context, tx Tx, id influxdb.ID, st string) error {
e := &influxdb.OperationLogEntry{
Description: st,
}
// 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 := icontext.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 s.addLogEntry(ctx, tx, k, v, s.time())
}
// UnexpectedBucketError is used when the error comes from an internal system.
func UnexpectedBucketError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving bucket's bucket; Err %v", err),
Op: "kv/bucketBucket",
}
}
// UnexpectedBucketIndexError is used when the error comes from an internal system.
func UnexpectedBucketIndexError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving bucket index; Err: %v", err),
Op: "kv/bucketIndex",
}
}
// BucketAlreadyExistsError is used when creating a bucket with a name
// that already exists within an organization.
func BucketAlreadyExistsError(b *influxdb.Bucket) error {
return &influxdb.Error{
Code: influxdb.EConflict,
Op: "kv/bucket",
Msg: fmt.Sprintf("bucket with name %s already exists", b.Name),
}
}

76
kv/bucket_test.go Normal file
View File

@ -0,0 +1,76 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltBucketService(t *testing.T) {
influxdbtesting.BucketService(initBoltBucketService, t)
}
func TestInmemBucketService(t *testing.T) {
influxdbtesting.BucketService(initInmemBucketService, t)
}
func initBoltBucketService(f influxdbtesting.BucketFields, t *testing.T) (influxdb.BucketService, string, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initBucketService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initInmemBucketService(f influxdbtesting.BucketFields, t *testing.T) (influxdb.BucketService, string, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initBucketService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initBucketService(s kv.Store, f influxdbtesting.BucketFields, t *testing.T) (influxdb.BucketService, string, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing bucket service: %v", err)
}
for _, o := range f.Organizations {
if err := svc.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate organizations")
}
}
for _, b := range f.Buckets {
if err := svc.PutBucket(ctx, b); err != nil {
t.Fatalf("failed to populate buckets")
}
}
return svc, kv.OpPrefix, func() {
for _, o := range f.Organizations {
if err := svc.DeleteOrganization(ctx, o.ID); err != nil {
t.Logf("failed to remove organization: %v", err)
}
}
for _, b := range f.Buckets {
if err := svc.DeleteBucket(ctx, b.ID); err != nil {
t.Logf("failed to remove bucket: %v", err)
}
}
}
}

960
kv/dashboard.go Normal file
View File

@ -0,0 +1,960 @@
package kv
import (
"bytes"
"context"
"encoding/json"
"time"
influxdb "github.com/influxdata/influxdb"
icontext "github.com/influxdata/influxdb/context"
)
var (
dashboardBucket = []byte("dashboardsv2")
orgDashboardIndex = []byte("orgsdashboardsv1")
dashboardCellViewBucket = []byte("dashboardcellviewsv1")
)
// TODO(desa): what do we want these to be?
const (
dashboardCreatedEvent = "Dashboard Created"
dashboardUpdatedEvent = "Dashboard Updated"
dashboardRemovedEvent = "Dashboard Removed"
dashboardCellsReplacedEvent = "Dashboard Cells Replaced"
dashboardCellAddedEvent = "Dashboard Cell Added"
dashboardCellRemovedEvent = "Dashboard Cell Removed"
dashboardCellUpdatedEvent = "Dashboard Cell Updated"
)
var _ influxdb.DashboardService = (*Service)(nil)
var _ influxdb.DashboardOperationLogService = (*Service)(nil)
func (s *Service) initializeDashboards(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket(dashboardBucket); err != nil {
return err
}
if _, err := tx.Bucket(orgDashboardIndex); err != nil {
return err
}
if _, err := tx.Bucket(dashboardCellViewBucket); err != nil {
return err
}
return nil
}
// FindDashboardByID retrieves a dashboard by id.
func (s *Service) FindDashboardByID(ctx context.Context, id influxdb.ID) (*influxdb.Dashboard, error) {
var d *influxdb.Dashboard
err := s.kv.View(func(tx Tx) error {
dash, err := s.findDashboardByID(ctx, tx, id)
if err != nil {
return err
}
d = dash
return nil
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return d, nil
}
func (s *Service) findDashboardByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Dashboard, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(dashboardBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrDashboardNotFound,
}
}
if err != nil {
return nil, err
}
var d influxdb.Dashboard
if err := json.Unmarshal(v, &d); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return &d, nil
}
// FindDashboard retrieves a dashboard using an arbitrary dashboard filter.
func (s *Service) FindDashboard(ctx context.Context, filter influxdb.DashboardFilter, opts ...influxdb.FindOptions) (*influxdb.Dashboard, error) {
if len(filter.IDs) == 1 {
return s.FindDashboardByID(ctx, *filter.IDs[0])
}
var d *influxdb.Dashboard
err := s.kv.View(func(tx Tx) error {
filterFn := filterDashboardsFn(filter)
return s.forEachDashboard(ctx, tx, opts[0].Descending, func(dash *influxdb.Dashboard) bool {
if filterFn(dash) {
d = dash
return false
}
return true
})
})
if err != nil {
return nil, err
}
if d == nil {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrDashboardNotFound,
}
}
return d, nil
}
func filterDashboardsFn(filter influxdb.DashboardFilter) func(d *influxdb.Dashboard) bool {
if len(filter.IDs) > 0 {
m := map[string]struct{}{}
for _, id := range filter.IDs {
m[id.String()] = struct{}{}
}
return func(d *influxdb.Dashboard) bool {
_, ok := m[d.ID.String()]
return ok
}
}
return func(d *influxdb.Dashboard) bool { return true }
}
// FindDashboards retrives all dashboards that match an arbitrary dashboard filter.
func (s *Service) FindDashboards(ctx context.Context, filter influxdb.DashboardFilter, opts influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) {
ds := []*influxdb.Dashboard{}
if len(filter.IDs) == 1 {
d, err := s.FindDashboardByID(ctx, *filter.IDs[0])
if err != nil && influxdb.ErrorCode(err) != influxdb.ENotFound {
return ds, 0, &influxdb.Error{
Err: err,
}
}
if d == nil {
return ds, 0, nil
}
return []*influxdb.Dashboard{d}, 1, nil
}
err := s.kv.View(func(tx Tx) error {
dashs, err := s.findDashboards(ctx, tx, filter, opts)
if err != nil && influxdb.ErrorCode(err) != influxdb.ENotFound {
return err
}
ds = dashs
return nil
})
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
influxdb.SortDashboards(opts, ds)
return ds, len(ds), nil
}
func (s *Service) findOrganizationDashboards(ctx context.Context, tx Tx, orgID influxdb.ID) ([]*influxdb.Dashboard, error) {
idx, err := tx.Bucket(orgDashboardIndex)
if err != nil {
return nil, err
}
// TODO(desa): support find options.
cur, err := idx.Cursor()
if err != nil {
return nil, err
}
prefix, err := orgID.Encode()
if err != nil {
return nil, err
}
ds := []*influxdb.Dashboard{}
for k, _ := cur.Seek(prefix); bytes.HasPrefix(k, prefix); k, _ = cur.Next() {
_, id, err := decodeOrgDashboardIndexKey(k)
if err != nil {
return nil, err
}
d, err := s.findDashboardByID(ctx, tx, id)
if err != nil {
return nil, err
}
ds = append(ds, d)
}
return ds, nil
}
func decodeOrgDashboardIndexKey(indexKey []byte) (orgID influxdb.ID, dashID influxdb.ID, err error) {
if len(indexKey) != 2*influxdb.IDLength {
return 0, 0, &influxdb.Error{Code: influxdb.EInternal, Msg: "malformed org dashboard index key (please report this error)"}
}
if err := (&orgID).Decode(indexKey[:influxdb.IDLength]); err != nil {
return 0, 0, &influxdb.Error{Code: influxdb.EInternal, Msg: "bad org id", Err: influxdb.ErrInvalidID}
}
if err := (&dashID).Decode(indexKey[influxdb.IDLength:]); err != nil {
return 0, 0, &influxdb.Error{Code: influxdb.EInternal, Msg: "bad dashboard id", Err: influxdb.ErrInvalidID}
}
return orgID, dashID, nil
}
func (s *Service) findDashboards(ctx context.Context, tx Tx, filter influxdb.DashboardFilter, opts ...influxdb.FindOptions) ([]*influxdb.Dashboard, error) {
if filter.OrganizationID != nil {
return s.findOrganizationDashboards(ctx, tx, *filter.OrganizationID)
}
if filter.Organization != nil {
o, err := s.findOrganizationByName(ctx, tx, *filter.Organization)
if err != nil {
return nil, err
}
return s.findOrganizationDashboards(ctx, tx, 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
}
ds := []*influxdb.Dashboard{}
filterFn := filterDashboardsFn(filter)
err := s.forEachDashboard(ctx, tx, descending, func(d *influxdb.Dashboard) bool {
if filterFn(d) {
if count >= offset {
ds = append(ds, d)
}
count++
}
if limit > 0 && len(ds) >= limit {
return false
}
return true
})
if err != nil {
return nil, err
}
return ds, nil
}
// CreateDashboard creates a influxdb dashboard and sets d.ID.
func (s *Service) CreateDashboard(ctx context.Context, d *influxdb.Dashboard) error {
err := s.kv.Update(func(tx Tx) error {
d.ID = s.IDGenerator.ID()
for _, cell := range d.Cells {
cell.ID = s.IDGenerator.ID()
if err := s.createCellView(ctx, tx, d.ID, cell.ID, nil); err != nil {
return err
}
}
if err := s.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCreatedEvent); err != nil {
return err
}
if err := s.putOrganizationDashboardIndex(ctx, tx, d); err != nil {
return err
}
// TODO(desa): don't populate this here. use the first/last methods of the oplog to get meta fields.
d.Meta.CreatedAt = s.time()
return s.putDashboardWithMeta(ctx, tx, d)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func (s *Service) createCellView(ctx context.Context, tx Tx, dashID, cellID influxdb.ID, view *influxdb.View) error {
if view == nil {
// If not view exists create the view
view = &influxdb.View{}
}
// TODO: this is temporary until we can fully remove the view service.
view.ID = cellID
return s.putDashboardCellView(ctx, tx, dashID, cellID, view)
}
// ReplaceDashboardCells updates the positions of each cell in a dashboard concurrently.
func (s *Service) ReplaceDashboardCells(ctx context.Context, id influxdb.ID, cs []*influxdb.Cell) error {
err := s.kv.Update(func(tx Tx) error {
d, err := s.findDashboardByID(ctx, tx, id)
if err != nil {
return err
}
ids := map[string]*influxdb.Cell{}
for _, cell := range d.Cells {
ids[cell.ID.String()] = cell
}
for _, cell := range cs {
if !cell.ID.Valid() {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "cannot provide empty cell id",
}
}
if _, ok := ids[cell.ID.String()]; !ok {
return &influxdb.Error{
Code: influxdb.EConflict,
Msg: "cannot replace cells that were not already present",
}
}
}
d.Cells = cs
if err := s.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCellsReplacedEvent); err != nil {
return err
}
return s.putDashboardWithMeta(ctx, tx, d)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func (s *Service) addDashboardCell(ctx context.Context, tx Tx, id influxdb.ID, cell *influxdb.Cell, opts influxdb.AddDashboardCellOptions) error {
d, err := s.findDashboardByID(ctx, tx, id)
if err != nil {
return err
}
cell.ID = s.IDGenerator.ID()
if err := s.createCellView(ctx, tx, id, cell.ID, opts.View); err != nil {
return err
}
d.Cells = append(d.Cells, cell)
if err := s.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCellAddedEvent); err != nil {
return err
}
return s.putDashboardWithMeta(ctx, tx, d)
}
// AddDashboardCell adds a cell to a dashboard and sets the cells ID.
func (s *Service) AddDashboardCell(ctx context.Context, id influxdb.ID, cell *influxdb.Cell, opts influxdb.AddDashboardCellOptions) error {
err := s.kv.Update(func(tx Tx) error {
return s.addDashboardCell(ctx, tx, id, cell, opts)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// RemoveDashboardCell removes a cell from a dashboard.
func (s *Service) RemoveDashboardCell(ctx context.Context, dashboardID, cellID influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
d, err := s.findDashboardByID(ctx, tx, dashboardID)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
idx := -1
for i, cell := range d.Cells {
if cell.ID == cellID {
idx = i
break
}
}
if idx == -1 {
return &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrCellNotFound,
}
}
if err := s.deleteDashboardCellView(ctx, tx, d.ID, d.Cells[idx].ID); err != nil {
return &influxdb.Error{
Err: err,
}
}
d.Cells = append(d.Cells[:idx], d.Cells[idx+1:]...)
if err := s.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCellRemovedEvent); err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := s.putDashboardWithMeta(ctx, tx, d); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
})
}
// GetDashboardCellView retrieves the view for a dashboard cell.
func (s *Service) GetDashboardCellView(ctx context.Context, dashboardID, cellID influxdb.ID) (*influxdb.View, error) {
var v *influxdb.View
err := s.kv.View(func(tx Tx) error {
view, err := s.findDashboardCellView(ctx, tx, dashboardID, cellID)
if err != nil {
return err
}
v = view
return nil
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return v, nil
}
func (s *Service) findDashboardCellView(ctx context.Context, tx Tx, dashboardID, cellID influxdb.ID) (*influxdb.View, error) {
k, err := encodeDashboardCellViewID(dashboardID, cellID)
if err != nil {
return nil, influxdb.NewError(influxdb.WithErrorErr(err))
}
vb, err := tx.Bucket(dashboardCellViewBucket)
if err != nil {
return nil, err
}
v, err := vb.Get(k)
if IsNotFound(err) {
return nil, influxdb.NewError(influxdb.WithErrorCode(influxdb.ENotFound), influxdb.WithErrorMsg(influxdb.ErrViewNotFound))
}
if err != nil {
return nil, err
}
view := &influxdb.View{}
if err := json.Unmarshal(v, view); err != nil {
return nil, influxdb.NewError(influxdb.WithErrorErr(err))
}
return view, nil
}
func (s *Service) deleteDashboardCellView(ctx context.Context, tx Tx, dashboardID, cellID influxdb.ID) error {
k, err := encodeDashboardCellViewID(dashboardID, cellID)
if err != nil {
return influxdb.NewError(influxdb.WithErrorErr(err))
}
vb, err := tx.Bucket(dashboardCellViewBucket)
if err != nil {
return err
}
if err := vb.Delete(k); err != nil {
return influxdb.NewError(influxdb.WithErrorErr(err))
}
return nil
}
func (s *Service) putDashboardCellView(ctx context.Context, tx Tx, dashboardID, cellID influxdb.ID, view *influxdb.View) error {
k, err := encodeDashboardCellViewID(dashboardID, cellID)
if err != nil {
return influxdb.NewError(influxdb.WithErrorErr(err))
}
v, err := json.Marshal(view)
if err != nil {
return influxdb.NewError(influxdb.WithErrorErr(err))
}
vb, err := tx.Bucket(dashboardCellViewBucket)
if err != nil {
return err
}
if err := vb.Put(k, v); err != nil {
return influxdb.NewError(influxdb.WithErrorErr(err))
}
return nil
}
func encodeDashboardCellViewID(dashID, cellID influxdb.ID) ([]byte, error) {
did, err := dashID.Encode()
if err != nil {
return nil, err
}
cid, err := cellID.Encode()
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(nil)
if _, err := buf.Write(did); err != nil {
return nil, err
}
if _, err := buf.Write(cid); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// UpdateDashboardCellView updates the view for a dashboard cell.
func (s *Service) UpdateDashboardCellView(ctx context.Context, dashboardID, cellID influxdb.ID, upd influxdb.ViewUpdate) (*influxdb.View, error) {
var v *influxdb.View
err := s.kv.Update(func(tx Tx) error {
view, err := s.findDashboardCellView(ctx, tx, dashboardID, cellID)
if err != nil {
return err
}
if err := upd.Apply(view); err != nil {
return err
}
if err := s.putDashboardCellView(ctx, tx, dashboardID, cellID, view); err != nil {
return err
}
v = view
return nil
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return v, nil
}
// UpdateDashboardCell udpates a cell on a dashboard.
func (s *Service) UpdateDashboardCell(ctx context.Context, dashboardID, cellID influxdb.ID, upd influxdb.CellUpdate) (*influxdb.Cell, error) {
if err := upd.Valid(); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
var cell *influxdb.Cell
err := s.kv.Update(func(tx Tx) error {
d, err := s.findDashboardByID(ctx, tx, dashboardID)
if err != nil {
return err
}
idx := -1
for i, cell := range d.Cells {
if cell.ID == cellID {
idx = i
break
}
}
if idx == -1 {
return &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrCellNotFound,
}
}
if err := upd.Apply(d.Cells[idx]); err != nil {
return err
}
cell = d.Cells[idx]
if err := s.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCellUpdatedEvent); err != nil {
return err
}
return s.putDashboardWithMeta(ctx, tx, d)
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return cell, nil
}
// PutDashboard will put a dashboard without setting an ID.
func (s *Service) PutDashboard(ctx context.Context, d *influxdb.Dashboard) error {
return s.kv.Update(func(tx Tx) error {
for _, cell := range d.Cells {
if err := s.createCellView(ctx, tx, d.ID, cell.ID, nil); err != nil {
return err
}
}
if err := s.putOrganizationDashboardIndex(ctx, tx, d); err != nil {
return err
}
return s.putDashboard(ctx, tx, d)
})
}
func encodeOrgDashboardIndex(orgID influxdb.ID, dashID influxdb.ID) ([]byte, error) {
oid, err := orgID.Encode()
if err != nil {
return nil, err
}
did, err := dashID.Encode()
if err != nil {
return nil, err
}
key := make([]byte, 0, len(oid)+len(did))
key = append(key, oid...)
key = append(key, did...)
return key, nil
}
func (s *Service) putOrganizationDashboardIndex(ctx context.Context, tx Tx, d *influxdb.Dashboard) error {
k, err := encodeOrgDashboardIndex(d.OrganizationID, d.ID)
if err != nil {
return err
}
idx, err := tx.Bucket(orgDashboardIndex)
if err != nil {
return err
}
if err := idx.Put(k, nil); err != nil {
return err
}
return nil
}
func (s *Service) removeOrganizationDashboardIndex(ctx context.Context, tx Tx, d *influxdb.Dashboard) error {
k, err := encodeOrgDashboardIndex(d.OrganizationID, d.ID)
if err != nil {
return err
}
idx, err := tx.Bucket(orgDashboardIndex)
if err != nil {
return err
}
if err := idx.Delete(k); err != nil {
return err
}
return nil
}
func (s *Service) putDashboard(ctx context.Context, tx Tx, d *influxdb.Dashboard) error {
v, err := json.Marshal(d)
if err != nil {
return err
}
encodedID, err := d.ID.Encode()
if err != nil {
return err
}
b, err := tx.Bucket(dashboardBucket)
if err != nil {
return err
}
if err := b.Put(encodedID, v); err != nil {
return err
}
return nil
}
func (s *Service) putDashboardWithMeta(ctx context.Context, tx Tx, d *influxdb.Dashboard) error {
// TODO(desa): don't populate this here. use the first/last methods of the oplog to get meta fields.
d.Meta.UpdatedAt = s.time()
return s.putDashboard(ctx, tx, d)
}
// forEachDashboard will iterate through all dashboards while fn returns true.
func (s *Service) forEachDashboard(ctx context.Context, tx Tx, descending bool, fn func(*influxdb.Dashboard) bool) error {
b, err := tx.Bucket(dashboardBucket)
if err != nil {
return err
}
cur, err := b.Cursor()
if err != nil {
return err
}
var k, v []byte
if descending {
k, v = cur.Last()
} else {
k, v = cur.First()
}
for k != nil {
d := &influxdb.Dashboard{}
if err := json.Unmarshal(v, d); err != nil {
return err
}
if !fn(d) {
break
}
if descending {
k, v = cur.Prev()
} else {
k, v = cur.Next()
}
}
return nil
}
// UpdateDashboard updates a dashboard according the parameters set on upd.
func (s *Service) UpdateDashboard(ctx context.Context, id influxdb.ID, upd influxdb.DashboardUpdate) (*influxdb.Dashboard, error) {
if err := upd.Valid(); err != nil {
return nil, err
}
var d *influxdb.Dashboard
err := s.kv.Update(func(tx Tx) error {
dash, err := s.updateDashboard(ctx, tx, id, upd)
if err != nil {
return err
}
d = dash
return nil
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return d, err
}
func (s *Service) updateDashboard(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.DashboardUpdate) (*influxdb.Dashboard, error) {
d, err := s.findDashboardByID(ctx, tx, id)
if err != nil {
return nil, err
}
if err := upd.Apply(d); err != nil {
return nil, err
}
if err := s.appendDashboardEventToLog(ctx, tx, d.ID, dashboardUpdatedEvent); err != nil {
return nil, err
}
if err := s.putDashboardWithMeta(ctx, tx, d); err != nil {
return nil, err
}
return d, nil
}
// DeleteDashboard deletes a dashboard and prunes it from the index.
func (s *Service) DeleteDashboard(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
if pe := s.deleteDashboard(ctx, tx, id); pe != nil {
return &influxdb.Error{
Err: pe,
}
}
return nil
})
}
func (s *Service) deleteDashboard(ctx context.Context, tx Tx, id influxdb.ID) error {
d, pe := s.findDashboardByID(ctx, tx, id)
if pe != nil {
return pe
}
for _, cell := range d.Cells {
if err := s.deleteDashboardCellView(ctx, tx, d.ID, cell.ID); err != nil {
return &influxdb.Error{
Err: err,
}
}
}
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := s.removeOrganizationDashboardIndex(ctx, tx, d); err != nil {
return influxdb.NewError(influxdb.WithErrorErr(err))
}
b, err := tx.Bucket(dashboardBucket)
if err != nil {
return err
}
if err := b.Delete(encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
err = s.deleteUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: id,
ResourceType: influxdb.DashboardsResourceType,
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := s.appendDashboardEventToLog(ctx, tx, d.ID, dashboardRemovedEvent); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
const dashboardOperationLogKeyPrefix = "dashboard"
func encodeDashboardOperationLogKey(id influxdb.ID) ([]byte, error) {
buf, err := id.Encode()
if err != nil {
return nil, err
}
return append([]byte(dashboardOperationLogKeyPrefix), buf...), nil
}
// GetDashboardOperationLog retrieves a dashboards operation log.
func (s *Service) GetDashboardOperationLog(ctx context.Context, id influxdb.ID, opts influxdb.FindOptions) ([]*influxdb.OperationLogEntry, int, error) {
// TODO(desa): might be worthwhile to allocate a slice of size opts.Limit
log := []*influxdb.OperationLogEntry{}
err := s.kv.View(func(tx Tx) error {
key, err := encodeDashboardOperationLogKey(id)
if err != nil {
return err
}
return s.forEachLogEntry(ctx, tx, key, opts, func(v []byte, t time.Time) error {
e := &influxdb.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
}
func (s *Service) appendDashboardEventToLog(ctx context.Context, tx Tx, id influxdb.ID, st string) error {
e := &influxdb.OperationLogEntry{
Description: st,
}
// 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 := icontext.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 := encodeDashboardOperationLogKey(id)
if err != nil {
return err
}
return s.addLogEntry(ctx, tx, k, v, s.time())
}

73
kv/dashboard_test.go Normal file
View File

@ -0,0 +1,73 @@
package kv_test
import (
"context"
"testing"
"time"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltDashboardService(t *testing.T) {
influxdbtesting.DashboardService(initBoltDashboardService, t)
}
func TestInmemDashboardService(t *testing.T) {
influxdbtesting.DashboardService(initInmemDashboardService, t)
}
func initBoltDashboardService(f influxdbtesting.DashboardFields, t *testing.T) (influxdb.DashboardService, string, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initDashboardService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initInmemDashboardService(f influxdbtesting.DashboardFields, t *testing.T) (influxdb.DashboardService, string, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initDashboardService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initDashboardService(s kv.Store, f influxdbtesting.DashboardFields, t *testing.T) (influxdb.DashboardService, string, func()) {
if f.NowFn == nil {
f.NowFn = time.Now
}
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
svc.WithTime(f.NowFn)
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing organization service: %v", err)
}
for _, b := range f.Dashboards {
if err := svc.PutDashboard(ctx, b); err != nil {
t.Fatalf("failed to populate dashboards")
}
}
return svc, kv.OpPrefix, func() {
for _, b := range f.Dashboards {
if err := svc.DeleteDashboard(ctx, b.ID); err != nil {
t.Logf("failed to remove dashboard: %v", err)
}
}
}
}

View File

@ -1,443 +0,0 @@
// Note: this file is used as a proof of concept for having a generic
// keyvalue store backed by specific implementations of kv.Store.
package kv
import (
"context"
"encoding/json"
"fmt"
platform "github.com/influxdata/influxdb"
)
var (
exampleBucket = []byte("examplesv1")
exampleIndex = []byte("exampleindexv1")
)
// ExampleService is an example user like service built on a generic kv store.
type ExampleService struct {
kv Store
idGenerator platform.IDGenerator
}
// NewExampleService creates an instance of an example service.
func NewExampleService(kv Store, idGen platform.IDGenerator) *ExampleService {
return &ExampleService{
kv: kv,
idGenerator: idGen,
}
}
// Initialize creates the buckets for the example service
func (c *ExampleService) Initialize() error {
return c.kv.Update(func(tx Tx) error {
if _, err := tx.Bucket([]byte(exampleBucket)); err != nil {
return err
}
if _, err := tx.Bucket([]byte(exampleIndex)); err != nil {
return err
}
return nil
})
}
// FindUserByID retrieves a example by id.
func (c *ExampleService) FindUserByID(ctx context.Context, id platform.ID) (*platform.User, error) {
var u *platform.User
err := c.kv.View(func(tx Tx) error {
usr, err := c.findUserByID(ctx, tx, id)
if err != nil {
return err
}
u = usr
return nil
})
if err != nil {
return nil, &platform.Error{
Op: "kv/" + platform.OpFindUserByID,
Err: err,
}
}
return u, nil
}
func (c *ExampleService) findUserByID(ctx context.Context, tx Tx, id platform.ID) (*platform.User, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, err
}
b, err := tx.Bucket(exampleBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if err == ErrKeyNotFound {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "user not found",
}
}
if err != nil {
return nil, err
}
var u platform.User
if err := json.Unmarshal(v, &u); err != nil {
return nil, err
}
return &u, nil
}
// FindUserByName returns a example by name for a particular example.
func (c *ExampleService) FindUserByName(ctx context.Context, n string) (*platform.User, error) {
var u *platform.User
err := c.kv.View(func(tx Tx) error {
usr, err := c.findUserByName(ctx, tx, n)
if err != nil {
return err
}
u = usr
return nil
})
return u, err
}
func (c *ExampleService) findUserByName(ctx context.Context, tx Tx, n string) (*platform.User, error) {
b, err := tx.Bucket(exampleIndex)
if err != nil {
return nil, err
}
uid, err := b.Get(exampleIndexKey(n))
if err == ErrKeyNotFound {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "user not found",
Op: "kv/" + platform.OpFindUser,
}
}
if err != nil {
return nil, err
}
var id platform.ID
if err := id.Decode(uid); err != nil {
return nil, err
}
return c.findUserByID(ctx, tx, id)
}
// FindUser retrives a example using an arbitrary example filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across examples until it finds a match.
func (c *ExampleService) FindUser(ctx context.Context, filter platform.UserFilter) (*platform.User, error) {
if filter.ID != nil {
u, err := c.FindUserByID(ctx, *filter.ID)
if err != nil {
return nil, &platform.Error{
Op: "kv/" + platform.OpFindUser,
Err: err,
}
}
return u, nil
}
if filter.Name != nil {
return c.FindUserByName(ctx, *filter.Name)
}
filterFn := filterExamplesFn(filter)
var u *platform.User
err := c.kv.View(func(tx Tx) error {
return forEachExample(ctx, tx, func(usr *platform.User) bool {
if filterFn(usr) {
u = usr
return false
}
return true
})
})
if err != nil {
return nil, err
}
if u == nil {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "user not found",
}
}
return u, nil
}
func filterExamplesFn(filter platform.UserFilter) func(u *platform.User) bool {
if filter.ID != nil {
return func(u *platform.User) bool {
return u.ID.Valid() && u.ID == *filter.ID
}
}
if filter.Name != nil {
return func(u *platform.User) bool {
return u.Name == *filter.Name
}
}
return func(u *platform.User) bool { return true }
}
// FindUsers retrives all examples that match an arbitrary example filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across all examples searching for a match.
func (c *ExampleService) FindUsers(ctx context.Context, filter platform.UserFilter, opt ...platform.FindOptions) ([]*platform.User, int, error) {
op := platform.OpFindUsers
if filter.ID != nil {
u, err := c.FindUserByID(ctx, *filter.ID)
if err != nil {
return nil, 0, &platform.Error{
Err: err,
Op: "kv/" + op,
}
}
return []*platform.User{u}, 1, nil
}
if filter.Name != nil {
u, err := c.FindUserByName(ctx, *filter.Name)
if err != nil {
return nil, 0, &platform.Error{
Err: err,
Op: "kv/" + op,
}
}
return []*platform.User{u}, 1, nil
}
us := []*platform.User{}
filterFn := filterExamplesFn(filter)
err := c.kv.View(func(tx Tx) error {
return forEachExample(ctx, tx, func(u *platform.User) bool {
if filterFn(u) {
us = append(us, u)
}
return true
})
})
if err != nil {
return nil, 0, err
}
return us, len(us), nil
}
// CreateUser creates a platform example and sets b.ID.
func (c *ExampleService) CreateUser(ctx context.Context, u *platform.User) error {
err := c.kv.Update(func(tx Tx) error {
unique := c.uniqueExampleName(ctx, tx, u)
if !unique {
// TODO: make standard error
return &platform.Error{
Code: platform.EConflict,
Msg: fmt.Sprintf("user with name %s already exists", u.Name),
}
}
u.ID = c.idGenerator.ID()
return c.putUser(ctx, tx, u)
})
if err != nil {
return &platform.Error{
Err: err,
Op: "kv/" + platform.OpCreateUser,
}
}
return nil
}
// PutUser will put a example without setting an ID.
func (c *ExampleService) PutUser(ctx context.Context, u *platform.User) error {
return c.kv.Update(func(tx Tx) error {
return c.putUser(ctx, tx, u)
})
}
func (c *ExampleService) putUser(ctx context.Context, tx Tx, u *platform.User) error {
v, err := json.Marshal(u)
if err != nil {
return err
}
encodedID, err := u.ID.Encode()
if err != nil {
return err
}
idx, err := tx.Bucket(exampleIndex)
if err != nil {
return err
}
if err := idx.Put(exampleIndexKey(u.Name), encodedID); err != nil {
return err
}
b, err := tx.Bucket(exampleBucket)
if err != nil {
return err
}
return b.Put(encodedID, v)
}
func exampleIndexKey(n string) []byte {
return []byte(n)
}
// forEachExample will iterate through all examples while fn returns true.
func forEachExample(ctx context.Context, tx Tx, fn func(*platform.User) bool) error {
b, err := tx.Bucket(exampleBucket)
if err != nil {
return err
}
cur, err := b.Cursor()
if err != nil {
return err
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
u := &platform.User{}
if err := json.Unmarshal(v, u); err != nil {
return err
}
if !fn(u) {
break
}
}
return nil
}
func (c *ExampleService) uniqueExampleName(ctx context.Context, tx Tx, u *platform.User) bool {
idx, err := tx.Bucket(exampleIndex)
if err != nil {
return false
}
if _, err := idx.Get(exampleIndexKey(u.Name)); err == ErrKeyNotFound {
return true
}
return false
}
// UpdateUser updates a example according the parameters set on upd.
func (c *ExampleService) UpdateUser(ctx context.Context, id platform.ID, upd platform.UserUpdate) (*platform.User, error) {
var u *platform.User
err := c.kv.Update(func(tx Tx) error {
usr, err := c.updateUser(ctx, tx, id, upd)
if err != nil {
return err
}
u = usr
return nil
})
if err != nil {
return nil, &platform.Error{
Err: err,
Op: "kv/" + platform.OpUpdateUser,
}
}
return u, nil
}
func (c *ExampleService) updateUser(ctx context.Context, tx Tx, id platform.ID, upd platform.UserUpdate) (*platform.User, error) {
u, err := c.findUserByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.Name != nil {
// Examples are indexed by name and so the example index must be pruned
// when name is modified.
idx, err := tx.Bucket(exampleIndex)
if err != nil {
return nil, err
}
if err := idx.Delete(exampleIndexKey(u.Name)); err != nil {
return nil, err
}
u.Name = *upd.Name
}
if err := c.putUser(ctx, tx, u); err != nil {
return nil, err
}
return u, nil
}
// DeleteUser deletes a example and prunes it from the index.
func (c *ExampleService) DeleteUser(ctx context.Context, id platform.ID) error {
err := c.kv.Update(func(tx Tx) error {
return c.deleteUser(ctx, tx, id)
})
if err != nil {
return &platform.Error{
Op: "kv/" + platform.OpDeleteUser,
Err: err,
}
}
return nil
}
func (c *ExampleService) deleteUser(ctx context.Context, tx Tx, id platform.ID) error {
u, err := c.findUserByID(ctx, tx, id)
if err != nil {
return err
}
encodedID, err := id.Encode()
if err != nil {
return err
}
idx, err := tx.Bucket(exampleIndex)
if err != nil {
return err
}
if err := idx.Delete(exampleIndexKey(u.Name)); err != nil {
return err
}
b, err := tx.Bucket(exampleBucket)
if err != nil {
return err
}
if err := b.Delete(encodedID); err != nil {
return err
}
return nil
}

37
kv/kv_test.go Normal file
View File

@ -0,0 +1,37 @@
package kv_test
import (
"context"
"errors"
"io/ioutil"
"os"
"github.com/influxdata/influxdb/bolt"
"github.com/influxdata/influxdb/inmem"
"github.com/influxdata/influxdb/kv"
)
func NewTestBoltStore() (kv.Store, func(), error) {
f, err := ioutil.TempFile("", "influxdata-bolt-")
if err != nil {
return nil, nil, errors.New("unable to open temporary boltdb file")
}
f.Close()
path := f.Name()
s := bolt.NewKVStore(path)
if err := s.Open(context.Background()); err != nil {
return nil, nil, err
}
close := func() {
s.Close()
os.Remove(path)
}
return s, close, nil
}
func NewTestInmemStore() (kv.Store, func(), error) {
return inmem.NewKVStore(), func() {}, nil
}

397
kv/kvlog.go Normal file
View File

@ -0,0 +1,397 @@
package kv
import (
"bytes"
"context"
"crypto/sha1"
"encoding/binary"
"encoding/json"
"fmt"
"time"
platform "github.com/influxdata/influxdb"
)
var (
kvlogBucket = []byte("keyvaluelogv1")
kvlogIndex = []byte("keyvaluelogindexv1")
)
var _ platform.KeyValueLog = (*Service)(nil)
type keyValueLogBounds struct {
Start int64 `json:"start"`
Stop int64 `json:"stop"`
}
func newKeyValueLogBounds(now time.Time) *keyValueLogBounds {
return &keyValueLogBounds{
Start: now.UTC().UnixNano(),
Stop: now.UTC().UnixNano(),
}
}
func (b *keyValueLogBounds) update(t time.Time) {
now := t.UTC().UnixNano()
if now < b.Start {
b.Start = now
} else if b.Stop < now {
b.Stop = now
}
}
// StartTime retrieves the start value of a bounds as a time.Time
func (b *keyValueLogBounds) StartTime() time.Time {
return time.Unix(0, b.Start)
}
// StopTime retrieves the stop value of a bounds as a time.Time
func (b *keyValueLogBounds) StopTime() time.Time {
return time.Unix(0, b.Stop)
}
// Bounds returns the key boundaries for the keyvaluelog for a resourceType/resourceID pair.
func (b *keyValueLogBounds) Bounds(k []byte) ([]byte, []byte, error) {
start, err := encodeLogEntryKey(k, b.Start)
if err != nil {
return nil, nil, err
}
stop, err := encodeLogEntryKey(k, b.Stop)
if err != nil {
return nil, nil, err
}
return start, stop, nil
}
func encodeLogEntryKey(key []byte, v int64) ([]byte, error) {
prefix := encodeKeyValueIndexKey(key)
k := make([]byte, len(prefix)+8)
buf := bytes.NewBuffer(k)
_, err := buf.Write(prefix)
if err != nil {
return nil, err
}
// This needs to be big-endian so that the iteration order is preserved when scanning keys
if err := binary.Write(buf, binary.BigEndian, v); err != nil {
return nil, err
}
return buf.Bytes(), err
}
func decodeLogEntryKey(key []byte) ([]byte, time.Time, error) {
buf := bytes.NewReader(key[len(key)-8:])
var ts int64
// This needs to be big-endian so that the iteration order is preserved when scanning keys
err := binary.Read(buf, binary.BigEndian, &ts)
if err != nil {
return nil, time.Unix(0, 0), err
}
return key[:len(key)-8], time.Unix(0, ts), nil
}
func encodeKeyValueIndexKey(k []byte) []byte {
// keys produced must be fixed length to ensure that we can iterate through the keyspace without any error.
h := sha1.New()
h.Write([]byte(k))
return h.Sum(nil)
}
func (s *Service) initializeKVLog(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket(kvlogBucket); err != nil {
return err
}
if _, err := tx.Bucket(kvlogIndex); err != nil {
return err
}
return nil
}
var errKeyValueLogBoundsNotFound = fmt.Errorf("oplog not found")
func (s *Service) getKeyValueLogBounds(ctx context.Context, tx Tx, key []byte) (*keyValueLogBounds, error) {
k := encodeKeyValueIndexKey(key)
b, err := tx.Bucket(kvlogIndex)
if err != nil {
return nil, err
}
v, err := b.Get(k)
if IsNotFound(err) {
return nil, errKeyValueLogBoundsNotFound
}
if err != nil {
return nil, err
}
bounds := &keyValueLogBounds{}
if err := json.Unmarshal(v, bounds); err != nil {
return nil, err
}
return bounds, nil
}
func (s *Service) putKeyValueLogBounds(ctx context.Context, tx Tx, key []byte, bounds *keyValueLogBounds) error {
k := encodeKeyValueIndexKey(key)
v, err := json.Marshal(bounds)
if err != nil {
return err
}
b, err := tx.Bucket(kvlogIndex)
if err != nil {
return err
}
if err := b.Put(k, v); err != nil {
return err
}
return nil
}
func (s *Service) updateKeyValueLogBounds(ctx context.Context, tx Tx, k []byte, t time.Time) error {
// retrieve the keyValue log boundaries
bounds, err := s.getKeyValueLogBounds(ctx, tx, k)
if err != nil && err != errKeyValueLogBoundsNotFound {
return err
}
if err == errKeyValueLogBoundsNotFound {
// if the bounds don't exist yet, create them
bounds = newKeyValueLogBounds(t)
}
// update the bounds to if needed
bounds.update(t)
if err := s.putKeyValueLogBounds(ctx, tx, k, bounds); err != nil {
return err
}
return nil
}
// ForEachLogEntry retrieves the keyValue log for a resource type ID combination. KeyValues may be returned in ascending and descending order.
func (s *Service) ForEachLogEntry(ctx context.Context, k []byte, opts platform.FindOptions, fn func([]byte, time.Time) error) error {
return s.kv.View(func(tx Tx) error {
return s.forEachLogEntry(ctx, tx, k, opts, fn)
})
}
func (s *Service) forEachLogEntry(ctx context.Context, tx Tx, k []byte, opts platform.FindOptions, fn func([]byte, time.Time) error) error {
b, err := s.getKeyValueLogBounds(ctx, tx, k)
if err != nil {
return err
}
bkt, err := tx.Bucket(kvlogBucket)
if err != nil {
return err
}
cur, err := bkt.Cursor()
if err != nil {
return err
}
next := cur.Next
startKey, stopKey, err := b.Bounds(k)
if err != nil {
return err
}
if opts.Descending {
next = cur.Prev
startKey, stopKey = stopKey, startKey
}
k, v := cur.Seek(startKey)
if !bytes.Equal(k, startKey) {
return fmt.Errorf("the first key not the key found in the log bounds. This should be impossible. Please report this error")
}
count := 0
if opts.Offset == 0 {
// Seek returns the kv at the position that was seeked to which should be the first element
// in the sequence of keyValues. If this condition is reached we need to start of iteration
// at 1 instead of 0.
_, ts, err := decodeLogEntryKey(k)
if err != nil {
return err
}
if err := fn(v, ts); err != nil {
return err
}
count++
if bytes.Equal(startKey, stopKey) {
// If the start and stop are the same, then there is only a single entry in the log
return nil
}
} else {
// Skip offset many items
for i := 0; i < opts.Offset-1; i++ {
k, _ := next()
if bytes.Equal(k, stopKey) {
return nil
}
}
}
for {
if count >= opts.Limit && opts.Limit != 0 {
break
}
k, v := next()
_, ts, err := decodeLogEntryKey(k)
if err != nil {
return err
}
if err := fn(v, ts); err != nil {
return err
}
if bytes.Equal(k, stopKey) {
// if we've reached the stop key, there are no keys log entries left
// in the keyspace.
break
}
count++
}
return nil
}
// AddLogEntry logs an keyValue for a particular resource type ID pairing.
func (s *Service) AddLogEntry(ctx context.Context, k, v []byte, t time.Time) error {
return s.kv.Update(func(tx Tx) error {
return s.addLogEntry(ctx, tx, k, v, t)
})
}
func (s *Service) addLogEntry(ctx context.Context, tx Tx, k, v []byte, t time.Time) error {
if err := s.updateKeyValueLogBounds(ctx, tx, k, t); err != nil {
return err
}
if err := s.putLogEntry(ctx, tx, k, v, t); err != nil {
return err
}
return nil
}
func (s *Service) putLogEntry(ctx context.Context, tx Tx, k, v []byte, t time.Time) error {
key, err := encodeLogEntryKey(k, t.UTC().UnixNano())
if err != nil {
return err
}
b, err := tx.Bucket(kvlogBucket)
if err != nil {
return err
}
if err := b.Put(key, v); err != nil {
return err
}
return nil
}
func (s *Service) getLogEntry(ctx context.Context, tx Tx, k []byte, t time.Time) ([]byte, time.Time, error) {
key, err := encodeLogEntryKey(k, t.UTC().UnixNano())
if err != nil {
return nil, t, err
}
b, err := tx.Bucket(kvlogBucket)
if err != nil {
return nil, t, err
}
v, err := b.Get(key)
if IsNotFound(err) {
return nil, t, fmt.Errorf("log entry not found")
}
if err != nil {
return nil, t, err
}
return v, t, nil
}
// FirstLogEntry retrieves the first log entry for a key value log.
func (s *Service) FirstLogEntry(ctx context.Context, k []byte) ([]byte, time.Time, error) {
var v []byte
var t time.Time
err := s.kv.View(func(tx Tx) error {
val, ts, err := s.firstLogEntry(ctx, tx, k)
if err != nil {
return err
}
v, t = val, ts
return nil
})
if err != nil {
return nil, t, err
}
return v, t, nil
}
// LastLogEntry retrieves the first log entry for a key value log.
func (s *Service) LastLogEntry(ctx context.Context, k []byte) ([]byte, time.Time, error) {
var v []byte
var t time.Time
err := s.kv.View(func(tx Tx) error {
val, ts, err := s.lastLogEntry(ctx, tx, k)
if err != nil {
return err
}
v, t = val, ts
return nil
})
if err != nil {
return nil, t, err
}
return v, t, nil
}
func (s *Service) firstLogEntry(ctx context.Context, tx Tx, k []byte) ([]byte, time.Time, error) {
bounds, err := s.getKeyValueLogBounds(ctx, tx, k)
if err != nil {
return nil, bounds.StartTime(), err
}
return s.getLogEntry(ctx, tx, k, bounds.StartTime())
}
func (s *Service) lastLogEntry(ctx context.Context, tx Tx, k []byte) ([]byte, time.Time, error) {
bounds, err := s.getKeyValueLogBounds(ctx, tx, k)
if err != nil {
return nil, bounds.StopTime(), err
}
return s.getLogEntry(ctx, tx, k, bounds.StopTime())
}

61
kv/kvlog_test.go Normal file
View File

@ -0,0 +1,61 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltKeyValueLog(t *testing.T) {
influxdbtesting.KeyValueLog(initBoltKeyValueLog, t)
}
func TestInmemKeyValueLog(t *testing.T) {
influxdbtesting.KeyValueLog(initInmemKeyValueLog, t)
}
func initBoltKeyValueLog(f influxdbtesting.KeyValueLogFields, t *testing.T) (influxdb.KeyValueLog, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initKeyValueLog(s, f, t)
return svc, func() {
closeSvc()
closeBolt()
}
}
func initInmemKeyValueLog(f influxdbtesting.KeyValueLogFields, t *testing.T) (influxdb.KeyValueLog, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initKeyValueLog(s, f, t)
return svc, func() {
closeSvc()
closeBolt()
}
}
func initKeyValueLog(s kv.Store, f influxdbtesting.KeyValueLogFields, t *testing.T) (influxdb.KeyValueLog, func()) {
svc := kv.NewService(s)
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing organization service: %v", err)
}
for _, e := range f.LogEntries {
if err := svc.AddLogEntry(ctx, e.Key, e.Value, e.Time); err != nil {
t.Fatalf("failed to populate log entries")
}
}
return svc, func() {
}
}

478
kv/label.go Normal file
View File

@ -0,0 +1,478 @@
package kv
import (
"bytes"
"context"
"encoding/json"
"github.com/influxdata/influxdb"
)
var (
labelBucket = []byte("labelsv1")
labelMappingBucket = []byte("labelmappingsv1")
)
func (s *Service) initializeLabels(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket(labelBucket); err != nil {
return err
}
if _, err := tx.Bucket(labelMappingBucket); err != nil {
return err
}
return nil
}
// FindLabelByID finds a label by its ID
func (s *Service) FindLabelByID(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) {
var l *influxdb.Label
err := s.kv.View(func(tx Tx) error {
label, pe := s.findLabelByID(ctx, tx, id)
if pe != nil {
return pe
}
l = label
return nil
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return l, nil
}
func (s *Service) findLabelByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Label, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(labelBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Err: influxdb.ErrLabelNotFound,
}
}
if err != nil {
return nil, err
}
var l influxdb.Label
if err := json.Unmarshal(v, &l); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return &l, nil
}
func filterLabelsFn(filter influxdb.LabelFilter) func(l *influxdb.Label) bool {
return func(label *influxdb.Label) bool {
return (filter.Name == "" || (filter.Name == label.Name))
}
}
// FindLabels returns a list of labels that match a filter.
func (s *Service) FindLabels(ctx context.Context, filter influxdb.LabelFilter, opt ...influxdb.FindOptions) ([]*influxdb.Label, error) {
ls := []*influxdb.Label{}
err := s.kv.View(func(tx Tx) error {
labels, err := s.findLabels(ctx, tx, filter)
if err != nil {
return err
}
ls = labels
return nil
})
if err != nil {
return nil, err
}
return ls, nil
}
func (s *Service) findLabels(ctx context.Context, tx Tx, filter influxdb.LabelFilter) ([]*influxdb.Label, error) {
ls := []*influxdb.Label{}
filterFn := filterLabelsFn(filter)
err := s.forEachLabel(ctx, tx, func(l *influxdb.Label) bool {
if filterFn(l) {
ls = append(ls, l)
}
return true
})
if err != nil {
return nil, err
}
return ls, nil
}
func decodeLabelMappingKey(key []byte) (resourceID influxdb.ID, labelID influxdb.ID, err error) {
if len(key) != 2*influxdb.IDLength {
return 0, 0, &influxdb.Error{Code: influxdb.EInvalid, Msg: "malformed label mapping key (please report this error)"}
}
if err := (&resourceID).Decode(key[:influxdb.IDLength]); err != nil {
return 0, 0, &influxdb.Error{Code: influxdb.EInvalid, Msg: "bad resource id", Err: influxdb.ErrInvalidID}
}
if err := (&labelID).Decode(key[influxdb.IDLength:]); err != nil {
return 0, 0, &influxdb.Error{Code: influxdb.EInvalid, Msg: "bad label id", Err: influxdb.ErrInvalidID}
}
return resourceID, labelID, nil
}
func (s *Service) FindResourceLabels(ctx context.Context, filter influxdb.LabelMappingFilter) ([]*influxdb.Label, error) {
if !filter.ResourceID.Valid() {
return nil, &influxdb.Error{Code: influxdb.EInvalid, Msg: "filter requires a valid resource id", Err: influxdb.ErrInvalidID}
}
ls := []*influxdb.Label{}
err := s.kv.View(func(tx Tx) error {
idx, err := tx.Bucket(labelMappingBucket)
if err != nil {
return err
}
cur, err := idx.Cursor()
if err != nil {
return err
}
prefix, err := filter.ResourceID.Encode()
if err != nil {
return err
}
for k, _ := cur.Seek(prefix); bytes.HasPrefix(k, prefix); k, _ = cur.Next() {
_, id, err := decodeLabelMappingKey(k)
if err != nil {
return err
}
l, err := s.findLabelByID(ctx, tx, id)
if l == nil && err != nil {
// TODO(jm): return error instead of continuing once orphaned mappings are fixed
// (see https://github.com/influxdata/influxdb/issues/11278)
continue
}
ls = append(ls, l)
}
return nil
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return ls, nil
}
// CreateLabelMapping creates a new mapping between a resource and a label.
func (s *Service) CreateLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
_, err := s.FindLabelByID(ctx, m.LabelID)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
err = s.kv.Update(func(tx Tx) error {
return s.putLabelMapping(ctx, tx, m)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// DeleteLabelMapping deletes a label mapping.
func (s *Service) DeleteLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
err := s.kv.Update(func(tx Tx) error {
return s.deleteLabelMapping(ctx, tx, m)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func (s *Service) deleteLabelMapping(ctx context.Context, tx Tx, m *influxdb.LabelMapping) error {
key, err := labelMappingKey(m)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
idx, err := tx.Bucket(labelMappingBucket)
if err != nil {
return err
}
if err := idx.Delete(key); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// CreateLabel creates a new label.
func (s *Service) CreateLabel(ctx context.Context, l *influxdb.Label) error {
err := s.kv.Update(func(tx Tx) error {
l.ID = s.IDGenerator.ID()
return s.putLabel(ctx, tx, l)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// PutLabel creates a label from the provided struct, without generating a new ID.
func (s *Service) PutLabel(ctx context.Context, l *influxdb.Label) error {
return s.kv.Update(func(tx Tx) error {
var err error
pe := s.putLabel(ctx, tx, l)
if pe != nil {
err = pe
}
return err
})
}
func labelMappingKey(m *influxdb.LabelMapping) ([]byte, error) {
lid, err := m.LabelID.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
rid, err := m.ResourceID.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
key := make([]byte, influxdb.IDLength+influxdb.IDLength) // len(rid) + len(lid)
copy(key, rid)
copy(key[len(rid):], lid)
return key, nil
}
func (s *Service) forEachLabel(ctx context.Context, tx Tx, fn func(*influxdb.Label) bool) error {
b, err := tx.Bucket(labelBucket)
if err != nil {
return err
}
cur, err := b.Cursor()
if err != nil {
return err
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
l := &influxdb.Label{}
if err := json.Unmarshal(v, l); err != nil {
return err
}
if !fn(l) {
break
}
}
return nil
}
// UpdateLabel updates a label.
func (s *Service) UpdateLabel(ctx context.Context, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
var label *influxdb.Label
err := s.kv.Update(func(tx Tx) error {
labelResponse, pe := s.updateLabel(ctx, tx, id, upd)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
label = labelResponse
return nil
})
return label, err
}
func (s *Service) updateLabel(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.LabelUpdate) (*influxdb.Label, error) {
label, err := s.findLabelByID(ctx, tx, id)
if err != nil {
return nil, err
}
if label.Properties == nil {
label.Properties = make(map[string]string)
}
for k, v := range upd.Properties {
if v == "" {
delete(label.Properties, k)
} else {
label.Properties[k] = v
}
}
if err := label.Validate(); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
if err := s.putLabel(ctx, tx, label); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return label, nil
}
// set a label and overwrite any existing label
func (s *Service) putLabel(ctx context.Context, tx Tx, l *influxdb.Label) error {
v, err := json.Marshal(l)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := l.ID.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(labelBucket)
if err != nil {
return err
}
if err := b.Put(encodedID, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// PutLabelMapping writes a label mapping to boltdb
func (s *Service) PutLabelMapping(ctx context.Context, m *influxdb.LabelMapping) error {
return s.kv.Update(func(tx Tx) error {
var err error
pe := s.putLabelMapping(ctx, tx, m)
if pe != nil {
err = pe
}
return err
})
}
func (s *Service) putLabelMapping(ctx context.Context, tx Tx, m *influxdb.LabelMapping) error {
v, err := json.Marshal(m)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
key, err := labelMappingKey(m)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
idx, err := tx.Bucket(labelMappingBucket)
if err != nil {
return err
}
if err := idx.Put(key, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// DeleteLabel deletes a label.
func (s *Service) DeleteLabel(ctx context.Context, id influxdb.ID) error {
err := s.kv.Update(func(tx Tx) error {
return s.deleteLabel(ctx, tx, id)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func (s *Service) deleteLabel(ctx context.Context, tx Tx, id influxdb.ID) error {
_, err := s.findLabelByID(ctx, tx, id)
if err != nil {
return err
}
encodedID, idErr := id.Encode()
if idErr != nil {
return &influxdb.Error{
Err: idErr,
}
}
b, err := tx.Bucket(labelBucket)
if err != nil {
return err
}
return b.Delete(encodedID)
}

73
kv/label_test.go Normal file
View File

@ -0,0 +1,73 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltLabelService(t *testing.T) {
influxdbtesting.LabelService(initBoltLabelService, t)
}
func TestInmemLabelService(t *testing.T) {
influxdbtesting.LabelService(initInmemLabelService, t)
}
func initBoltLabelService(f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initLabelService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initInmemLabelService(f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initLabelService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initLabelService(s kv.Store, f influxdbtesting.LabelFields, t *testing.T) (influxdb.LabelService, string, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing label service: %v", err)
}
for _, l := range f.Labels {
if err := svc.PutLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels: %v", err)
}
}
for _, m := range f.Mappings {
if err := svc.PutLabelMapping(ctx, m); err != nil {
t.Fatalf("failed to populate label mappings: %v", err)
}
}
return svc, kv.OpPrefix, func() {
for _, l := range f.Labels {
if err := svc.DeleteLabel(ctx, l.ID); err != nil {
t.Logf("failed to remove label: %v", err)
}
}
}
}

63
kv/lookup_service.go Normal file
View File

@ -0,0 +1,63 @@
package kv
import (
"context"
"github.com/influxdata/influxdb"
)
var _ influxdb.LookupService = (*Service)(nil)
// Name returns the name for the resource and ID.
func (s *Service) Name(ctx context.Context, resource influxdb.ResourceType, id influxdb.ID) (string, error) {
if err := resource.Valid(); err != nil {
return "", err
}
if ok := id.Valid(); !ok {
return "", influxdb.ErrInvalidID
}
switch resource {
case influxdb.TasksResourceType: // 5 // TODO(goller): unify task storage here so we can lookup names
case influxdb.AuthorizationsResourceType: // 0 TODO(goller): authorizations should also have optional names
case influxdb.BucketsResourceType: // 1
r, err := s.FindBucketByID(ctx, id)
if err != nil {
return "", err
}
return r.Name, nil
case influxdb.DashboardsResourceType: // 2
r, err := s.FindDashboardByID(ctx, id)
if err != nil {
return "", err
}
return r.Name, nil
case influxdb.OrgsResourceType: // 3
r, err := s.FindOrganizationByID(ctx, id)
if err != nil {
return "", err
}
return r.Name, nil
case influxdb.SourcesResourceType: // 4
r, err := s.FindSourceByID(ctx, id)
if err != nil {
return "", err
}
return r.Name, nil
case influxdb.TelegrafsResourceType: // 6
r, err := s.FindTelegrafConfigByID(ctx, id)
if err != nil {
return "", err
}
return r.Name, nil
case influxdb.UsersResourceType: // 7
r, err := s.FindUserByID(ctx, id)
if err != nil {
return "", err
}
return r.Name, nil
}
return "", nil
}

266
kv/lookup_service_test.go Normal file
View File

@ -0,0 +1,266 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
"github.com/influxdata/influxdb/mock"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
var (
testID = influxdb.ID(1)
testIDStr = testID.String()
)
type StoreFn func() (kv.Store, func(), error)
func TestLookupService_Name_WithBolt(t *testing.T) {
testLookupName(NewTestBoltStore, t)
}
func TestLookupService_Name_WithInMem(t *testing.T) {
testLookupName(NewTestInmemStore, t)
}
func testLookupName(newStore StoreFn, t *testing.T) {
type initFn func(context.Context, *kv.Service) error
type args struct {
resource influxdb.Resource
init initFn
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "error if id is invalid",
args: args{
resource: influxdb.Resource{
Type: influxdb.DashboardsResourceType,
ID: influxdbtesting.IDPtr(influxdb.InvalidID()),
},
},
wantErr: true,
},
{
name: "error if resource is invalid",
args: args{
resource: influxdb.Resource{
Type: influxdb.ResourceType("invalid"),
},
},
wantErr: true,
},
{
name: "authorization resource without a name returns empty string",
args: args{
resource: influxdb.Resource{
Type: influxdb.AuthorizationsResourceType,
ID: influxdbtesting.IDPtr(testID),
},
},
want: "",
},
{
name: "task resource without a name returns empty string",
args: args{
resource: influxdb.Resource{
Type: influxdb.TasksResourceType,
ID: influxdbtesting.IDPtr(testID),
},
},
want: "",
},
{
name: "bucket with existing id returns name",
args: args{
resource: influxdb.Resource{
Type: influxdb.BucketsResourceType,
ID: influxdbtesting.IDPtr(testID),
},
init: func(ctx context.Context, s *kv.Service) error {
_ = s.CreateOrganization(ctx, &influxdb.Organization{
Name: "o1",
})
return s.CreateBucket(ctx, &influxdb.Bucket{
Name: "b1",
OrganizationID: testID,
})
},
},
want: "b1",
},
{
name: "bucket with non-existent id returns error",
args: args{
resource: influxdb.Resource{
Type: influxdb.BucketsResourceType,
ID: influxdbtesting.IDPtr(testID),
},
},
wantErr: true,
},
{
name: "dashboard with existing id returns name",
args: args{
resource: influxdb.Resource{
Type: influxdb.DashboardsResourceType,
ID: influxdbtesting.IDPtr(testID),
},
init: func(ctx context.Context, s *kv.Service) error {
return s.CreateDashboard(ctx, &influxdb.Dashboard{
Name: "dashboard1",
OrganizationID: 1,
})
},
},
want: "dashboard1",
},
{
name: "dashboard with non-existent id returns error",
args: args{
resource: influxdb.Resource{
Type: influxdb.DashboardsResourceType,
ID: influxdbtesting.IDPtr(testID),
},
},
wantErr: true,
},
{
name: "org with existing id returns name",
args: args{
resource: influxdb.Resource{
Type: influxdb.OrgsResourceType,
ID: influxdbtesting.IDPtr(testID),
},
init: func(ctx context.Context, s *kv.Service) error {
return s.CreateOrganization(ctx, &influxdb.Organization{
Name: "org1",
})
},
},
want: "org1",
},
{
name: "org with non-existent id returns error",
args: args{
resource: influxdb.Resource{
Type: influxdb.OrgsResourceType,
ID: influxdbtesting.IDPtr(testID),
},
},
wantErr: true,
},
{
name: "source with existing id returns name",
args: args{
resource: influxdb.Resource{
Type: influxdb.SourcesResourceType,
ID: influxdbtesting.IDPtr(testID),
},
init: func(ctx context.Context, s *kv.Service) error {
return s.CreateSource(ctx, &influxdb.Source{
Name: "source1",
})
},
},
want: "source1",
},
{
name: "source with non-existent id returns error",
args: args{
resource: influxdb.Resource{
Type: influxdb.SourcesResourceType,
ID: influxdbtesting.IDPtr(testID),
},
},
wantErr: true,
},
{
name: "telegraf with existing id returns name",
args: args{
resource: influxdb.Resource{
Type: influxdb.TelegrafsResourceType,
ID: influxdbtesting.IDPtr(testID),
},
init: func(ctx context.Context, s *kv.Service) error {
return s.CreateTelegrafConfig(ctx, &influxdb.TelegrafConfig{
OrganizationID: influxdbtesting.MustIDBase16("0000000000000009"),
Name: "telegraf1",
}, testID)
},
},
want: "telegraf1",
},
{
name: "telegraf with non-existent id returns error",
args: args{
resource: influxdb.Resource{
Type: influxdb.TelegrafsResourceType,
ID: influxdbtesting.IDPtr(testID),
},
},
wantErr: true,
},
{
name: "user with existing id returns name",
args: args{
resource: influxdb.Resource{
Type: influxdb.UsersResourceType,
ID: influxdbtesting.IDPtr(testID),
},
init: func(ctx context.Context, s *kv.Service) error {
return s.CreateUser(ctx, &influxdb.User{
Name: "user1",
})
},
},
want: "user1",
},
{
name: "user with non-existent id returns error",
args: args{
resource: influxdb.Resource{
Type: influxdb.UsersResourceType,
ID: influxdbtesting.IDPtr(testID),
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store, done, err := newStore()
if err != nil {
t.Fatalf("unable to create bolt test client: %v", err)
}
svc := kv.NewService(store)
defer done()
svc.IDGenerator = mock.NewIDGenerator(testIDStr, t)
ctx := context.Background()
if tt.args.init != nil {
if err := tt.args.init(ctx, svc); err != nil {
t.Errorf("Service.Name() unable to initialize service: %v", err)
}
}
id := influxdb.InvalidID()
if tt.args.resource.ID != nil {
id = *tt.args.resource.ID
}
got, err := svc.Name(ctx, tt.args.resource.Type, id)
if (err != nil) != tt.wantErr {
t.Errorf("Service.Name() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Service.Name() = %v, want %v", got, tt.want)
}
})
}
}

174
kv/onboarding.go Normal file
View File

@ -0,0 +1,174 @@
package kv
import (
"context"
"fmt"
"time"
"github.com/influxdata/influxdb"
)
var (
onboardingBucket = []byte("onboardingv1")
onboardingKey = []byte("onboarding_key")
)
var _ influxdb.OnboardingService = (*Service)(nil)
func (s *Service) initializeOnboarding(ctx context.Context, tx Tx) error {
_, err := tx.Bucket(onboardingBucket)
return err
}
// IsOnboarding means if the initial setup of influxdb has happened.
// true means that the onboarding setup has not yet happened.
// false means that the onboarding has been completed.
func (s *Service) IsOnboarding(ctx context.Context) (bool, error) {
notSetup := true
err := s.kv.View(func(tx Tx) error {
bucket, err := tx.Bucket(onboardingBucket)
if err != nil {
return err
}
v, err := bucket.Get(onboardingKey)
// If the sentinel onboarding key is not found, then, setup
// has not been performed.
if IsNotFound(err) {
notSetup = true
return nil
}
if err != nil {
return err
}
// If the sentinel key has any bytes whatsoever, then,
if len(v) > 0 {
notSetup = false // this means that it is setup. I hate bools.
}
return nil
})
return notSetup, err
}
// PutOnboardingStatus will update the flag,
// so future onboarding request will be denied.
// true means that onboarding is NOT needed.
// false means that onboarding is needed.
func (s *Service) PutOnboardingStatus(ctx context.Context, hasBeenOnboarded bool) error {
return s.kv.Update(func(tx Tx) error {
return s.putOnboardingStatus(ctx, tx, hasBeenOnboarded)
})
}
func (s *Service) putOnboardingStatus(ctx context.Context, tx Tx, hasBeenOnboarded bool) error {
if hasBeenOnboarded {
return s.setOnboarded(ctx, tx)
}
return s.setOffboarded(ctx, tx)
}
func (s *Service) setOffboarded(ctx context.Context, tx Tx) error {
bucket, err := tx.Bucket(onboardingBucket)
if err != nil {
// TODO(goller): check err
return err
}
err = bucket.Delete(onboardingKey)
if err != nil {
// TODO(goller): check err
return err
}
return nil
}
func (s *Service) setOnboarded(ctx context.Context, tx Tx) error {
bucket, err := tx.Bucket(onboardingBucket)
if err != nil {
// TODO(goller): check err
return err
}
err = bucket.Put(onboardingKey, []byte{0x1})
if err != nil {
// TODO(goller): check err
return err
}
return nil
}
// Generate OnboardingResults from onboarding request,
// update db so this request will be disabled for the second run.
func (s *Service) Generate(ctx context.Context, req *influxdb.OnboardingRequest) (*influxdb.OnboardingResults, error) {
isOnboarding, err := s.IsOnboarding(ctx)
if err != nil {
return nil, err
}
if !isOnboarding {
return nil, &influxdb.Error{
Code: influxdb.EConflict,
Msg: "onboarding has already been completed",
}
}
if err := req.Valid(); err != nil {
return nil, err
}
u := &influxdb.User{Name: req.User}
o := &influxdb.Organization{Name: req.Org}
bucket := &influxdb.Bucket{
Name: req.Bucket,
Organization: o.Name,
RetentionPeriod: time.Duration(req.RetentionPeriod) * time.Hour,
}
mapping := &influxdb.UserResourceMapping{
ResourceType: influxdb.OrgsResourceType,
UserType: influxdb.Owner,
}
auth := &influxdb.Authorization{
Description: fmt.Sprintf("%s's Token", u.Name),
Permissions: influxdb.OperPermissions(),
Token: req.Token,
}
err = s.kv.Update(func(tx Tx) error {
if err := s.createUser(ctx, tx, u); err != nil {
return err
}
if err := s.setPassword(ctx, tx, u.Name, req.Password); err != nil {
return err
}
if err := s.createOrganization(ctx, tx, o); err != nil {
return err
}
bucket.OrganizationID = o.ID
if err := s.createBucket(ctx, tx, bucket); err != nil {
return err
}
mapping.ResourceID = o.ID
mapping.UserID = u.ID
if err := s.createUserResourceMapping(ctx, tx, mapping); err != nil {
return err
}
auth.UserID = u.ID
auth.OrgID = o.ID
if err := s.createAuthorization(ctx, tx, auth); err != nil {
return err
}
return s.putOnboardingStatus(ctx, tx, true)
})
if err != nil {
return nil, err
}
return &influxdb.OnboardingResults{
User: u,
Org: o,
Bucket: bucket,
Auth: auth,
}, nil
}

65
kv/onboarding_test.go Normal file
View File

@ -0,0 +1,65 @@
package kv_test
import (
"context"
"testing"
influxdb "github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltOnboardingService(t *testing.T) {
influxdbtesting.Generate(initBoltOnboardingService, t)
}
func TestInmemOnboardingService(t *testing.T) {
influxdbtesting.Generate(initInmemOnboardingService, t)
}
func initBoltOnboardingService(f influxdbtesting.OnboardingFields, t *testing.T) (influxdb.OnboardingService, func()) {
s, closeStore, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new bolt kv store: %v", err)
}
svc, closeSvc := initOnboardingService(s, f, t)
return svc, func() {
closeSvc()
closeStore()
}
}
func initInmemOnboardingService(f influxdbtesting.OnboardingFields, t *testing.T) (influxdb.OnboardingService, func()) {
s, closeStore, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new inmem kv store: %v", err)
}
svc, closeSvc := initOnboardingService(s, f, t)
return svc, func() {
closeSvc()
closeStore()
}
}
func initOnboardingService(s kv.Store, f influxdbtesting.OnboardingFields, t *testing.T) (influxdb.OnboardingService, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
svc.TokenGenerator = f.TokenGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("unable to initialize kv store: %v", err)
}
t.Logf("Onboarding: %v", f.IsOnboarding)
if err := svc.PutOnboardingStatus(ctx, !f.IsOnboarding); err != nil {
t.Fatalf("failed to set new onboarding finished: %v", err)
}
return svc, func() {
if err := svc.PutOnboardingStatus(ctx, false); err != nil {
t.Logf("failed to remove onboarding finished: %v", err)
}
}
}

615
kv/org.go Normal file
View File

@ -0,0 +1,615 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"time"
influxdb "github.com/influxdata/influxdb"
icontext "github.com/influxdata/influxdb/context"
)
var (
organizationBucket = []byte("organizationsv1")
organizationIndex = []byte("organizationindexv1")
)
var _ influxdb.OrganizationService = (*Service)(nil)
var _ influxdb.OrganizationOperationLogService = (*Service)(nil)
func (s *Service) initializeOrgs(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket(organizationBucket); err != nil {
return err
}
if _, err := tx.Bucket(organizationIndex); err != nil {
return err
}
return nil
}
// FindOrganizationByID retrieves a organization by id.
func (s *Service) FindOrganizationByID(ctx context.Context, id influxdb.ID) (*influxdb.Organization, error) {
var o *influxdb.Organization
err := s.kv.View(func(tx Tx) error {
org, pe := s.findOrganizationByID(ctx, tx, id)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
o = org
return nil
})
if err != nil {
return nil, err
}
return o, nil
}
func (s *Service) findOrganizationByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Organization, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
b, err := tx.Bucket(organizationBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "organization not found",
}
}
if err != nil {
return nil, err
}
var o influxdb.Organization
if err := json.Unmarshal(v, &o); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return &o, nil
}
// FindOrganizationByName returns a organization by name for a particular organization.
func (s *Service) FindOrganizationByName(ctx context.Context, n string) (*influxdb.Organization, error) {
var o *influxdb.Organization
err := s.kv.View(func(tx Tx) error {
org, err := s.findOrganizationByName(ctx, tx, n)
if err != nil {
return err
}
o = org
return nil
})
return o, err
}
func (s *Service) findOrganizationByName(ctx context.Context, tx Tx, n string) (*influxdb.Organization, error) {
b, err := tx.Bucket(organizationIndex)
if err != nil {
return nil, err
}
o, err := b.Get(organizationIndexKey(n))
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: fmt.Sprintf("organization name \"%s\" not found", n),
}
}
if err != nil {
return nil, err
}
var id influxdb.ID
if err := id.Decode(o); err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
return s.findOrganizationByID(ctx, tx, id)
}
// FindOrganization retrives a organization using an arbitrary organization filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across organizations until it finds a match.
func (s *Service) FindOrganization(ctx context.Context, filter influxdb.OrganizationFilter) (*influxdb.Organization, error) {
if filter.ID != nil {
return s.FindOrganizationByID(ctx, *filter.ID)
}
if filter.Name != nil {
return s.FindOrganizationByName(ctx, *filter.Name)
}
filterFn := filterOrganizationsFn(filter)
var o *influxdb.Organization
err := s.kv.View(func(tx Tx) error {
return forEachOrganization(ctx, tx, func(org *influxdb.Organization) bool {
if filterFn(org) {
o = org
return false
}
return true
})
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
if o == nil {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "organization not found",
}
}
return o, nil
}
func filterOrganizationsFn(filter influxdb.OrganizationFilter) func(o *influxdb.Organization) bool {
if filter.ID != nil {
return func(o *influxdb.Organization) bool {
return o.ID == *filter.ID
}
}
if filter.Name != nil {
return func(o *influxdb.Organization) bool {
return o.Name == *filter.Name
}
}
return func(o *influxdb.Organization) bool { return true }
}
// FindOrganizations retrives all organizations that match an arbitrary organization filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across all organizations searching for a match.
func (s *Service) FindOrganizations(ctx context.Context, filter influxdb.OrganizationFilter, opt ...influxdb.FindOptions) ([]*influxdb.Organization, int, error) {
if filter.ID != nil {
o, err := s.FindOrganizationByID(ctx, *filter.ID)
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return []*influxdb.Organization{o}, 1, nil
}
if filter.Name != nil {
o, err := s.FindOrganizationByName(ctx, *filter.Name)
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return []*influxdb.Organization{o}, 1, nil
}
os := []*influxdb.Organization{}
filterFn := filterOrganizationsFn(filter)
err := s.kv.View(func(tx Tx) error {
return forEachOrganization(ctx, tx, func(o *influxdb.Organization) bool {
if filterFn(o) {
os = append(os, o)
}
return true
})
})
if err != nil {
return nil, 0, &influxdb.Error{
Err: err,
}
}
return os, len(os), nil
}
// CreateOrganization creates a influxdb organization and sets b.ID.
func (s *Service) CreateOrganization(ctx context.Context, o *influxdb.Organization) error {
return s.kv.Update(func(tx Tx) error {
return s.createOrganization(ctx, tx, o)
})
}
func (s *Service) createOrganization(ctx context.Context, tx Tx, o *influxdb.Organization) error {
if err := s.uniqueOrganizationName(ctx, tx, o); err != nil {
return err
}
o.ID = s.IDGenerator.ID()
if err := s.appendOrganizationEventToLog(ctx, tx, o.ID, organizationCreatedEvent); err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := s.putOrganization(ctx, tx, o); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// PutOrganization will put a organization without setting an ID.
func (s *Service) PutOrganization(ctx context.Context, o *influxdb.Organization) error {
var err error
return s.kv.Update(func(tx Tx) error {
if pe := s.putOrganization(ctx, tx, o); pe != nil {
err = pe
}
return err
})
}
func (s *Service) putOrganization(ctx context.Context, tx Tx, o *influxdb.Organization) error {
v, err := json.Marshal(o)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := o.ID.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
idx, err := tx.Bucket(organizationIndex)
if err != nil {
return err
}
if err := idx.Put(organizationIndexKey(o.Name), encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(organizationBucket)
if err != nil {
return err
}
if err = b.Put(encodedID, v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func organizationIndexKey(n string) []byte {
return []byte(n)
}
// forEachOrganization will iterate through all organizations while fn returns true.
func forEachOrganization(ctx context.Context, tx Tx, fn func(*influxdb.Organization) bool) error {
b, err := tx.Bucket(organizationBucket)
if err != nil {
return err
}
cur, err := b.Cursor()
if err != nil {
return err
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
o := &influxdb.Organization{}
if err := json.Unmarshal(v, o); err != nil {
return err
}
if !fn(o) {
break
}
}
return nil
}
func (s *Service) uniqueOrganizationName(ctx context.Context, tx Tx, o *influxdb.Organization) error {
key := organizationIndexKey(o.Name)
// if the name is not unique across all organizations, then, do not
// allow creation.
err := s.unique(ctx, tx, organizationIndex, key)
if err == NotUniqueError {
return OrgAlreadyExistsError(o)
}
return err
}
// UpdateOrganization updates a organization according the parameters set on upd.
func (s *Service) UpdateOrganization(ctx context.Context, id influxdb.ID, upd influxdb.OrganizationUpdate) (*influxdb.Organization, error) {
var o *influxdb.Organization
err := s.kv.Update(func(tx Tx) error {
org, pe := s.updateOrganization(ctx, tx, id, upd)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
o = org
return nil
})
return o, err
}
func (s *Service) updateOrganization(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.OrganizationUpdate) (*influxdb.Organization, error) {
o, pe := s.findOrganizationByID(ctx, tx, id)
if pe != nil {
return nil, pe
}
if upd.Name != nil {
// Organizations are indexed by name and so the organization index must be pruned
// when name is modified.
idx, err := tx.Bucket(organizationIndex)
if err != nil {
return nil, err
}
if err := idx.Delete(organizationIndexKey(o.Name)); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
o.Name = *upd.Name
}
if err := s.appendOrganizationEventToLog(ctx, tx, o.ID, organizationUpdatedEvent); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
if pe := s.putOrganization(ctx, tx, o); pe != nil {
return nil, pe
}
return o, nil
}
// DeleteOrganization deletes a organization and prunes it from the index.
func (s *Service) DeleteOrganization(ctx context.Context, id influxdb.ID) error {
err := s.kv.Update(func(tx Tx) error {
//if pe := s.deleteOrganizationsBuckets(ctx, tx, id); pe != nil {
// return pe
//}
if pe := s.deleteOrganization(ctx, tx, id); pe != nil {
return pe
}
return nil
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func (s *Service) deleteOrganization(ctx context.Context, tx Tx, id influxdb.ID) error {
o, pe := s.findOrganizationByID(ctx, tx, id)
if pe != nil {
return pe
}
idx, err := tx.Bucket(organizationIndex)
if err != nil {
return err
}
if err := idx.Delete(organizationIndexKey(o.Name)); err != nil {
return &influxdb.Error{
Err: err,
}
}
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
b, err := tx.Bucket(organizationBucket)
if err != nil {
return err
}
if err = b.Delete(encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
//func (s *Service) deleteOrganizationsBuckets(ctx context.Context, tx Tx, id influxdb.ID) error {
// filter := influxdb.BucketFilter{
// OrganizationID: &id,
// }
// bs, pe := s.findBuckets(ctx, tx, filter)
// if pe != nil {
// return pe
// }
// for _, b := range bs {
// if pe := s.deleteBucket(ctx, tx, b.ID); pe != nil {
// return pe
// }
// }
// return nil
//}
// GetOrganizationOperationLog retrieves a organization operation log.
func (s *Service) GetOrganizationOperationLog(ctx context.Context, id influxdb.ID, opts influxdb.FindOptions) ([]*influxdb.OperationLogEntry, int, error) {
// TODO(desa): might be worthwhile to allocate a slice of size opts.Limit
log := []*influxdb.OperationLogEntry{}
err := s.kv.View(func(tx Tx) error {
key, err := encodeOrganizationOperationLogKey(id)
if err != nil {
return err
}
return s.forEachLogEntry(ctx, tx, key, opts, func(v []byte, t time.Time) error {
e := &influxdb.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 (
organizationCreatedEvent = "Organization Created"
organizationUpdatedEvent = "Organization Updated"
)
const orgOperationLogKeyPrefix = "org"
func encodeOrganizationOperationLogKey(id influxdb.ID) ([]byte, error) {
buf, err := id.Encode()
if err != nil {
return nil, err
}
return append([]byte(orgOperationLogKeyPrefix), buf...), nil
}
func (s *Service) appendOrganizationEventToLog(ctx context.Context, tx Tx, id influxdb.ID, st string) error {
e := &influxdb.OperationLogEntry{
Description: st,
}
// 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 organizationID will exist explicitly.
a, err := icontext.GetAuthorizer(ctx)
if err == nil {
// Add the organization 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 := encodeOrganizationOperationLogKey(id)
if err != nil {
return err
}
return s.addLogEntry(ctx, tx, k, v, s.time())
}
// FindResourceOrganizationID is used to find the organization that a resource belongs to five the id of a resource and a resource type.
func (s *Service) FindResourceOrganizationID(ctx context.Context, rt influxdb.ResourceType, id influxdb.ID) (influxdb.ID, error) {
switch rt {
case influxdb.AuthorizationsResourceType:
r, err := s.FindAuthorizationByID(ctx, id)
if err != nil {
return influxdb.InvalidID(), err
}
return r.OrgID, nil
case influxdb.BucketsResourceType:
r, err := s.FindBucketByID(ctx, id)
if err != nil {
return influxdb.InvalidID(), err
}
return r.OrganizationID, nil
case influxdb.OrgsResourceType:
r, err := s.FindOrganizationByID(ctx, id)
if err != nil {
return influxdb.InvalidID(), err
}
return r.ID, nil
case influxdb.DashboardsResourceType:
r, err := s.FindDashboardByID(ctx, id)
if err != nil {
return influxdb.InvalidID(), err
}
return r.OrganizationID, nil
case influxdb.SourcesResourceType:
r, err := s.FindSourceByID(ctx, id)
if err != nil {
return influxdb.InvalidID(), err
}
return r.OrganizationID, nil
case influxdb.TelegrafsResourceType:
r, err := s.FindTelegrafConfigByID(ctx, id)
if err != nil {
return influxdb.InvalidID(), err
}
return r.OrganizationID, nil
case influxdb.VariablesResourceType:
r, err := s.FindVariableByID(ctx, id)
if err != nil {
return influxdb.InvalidID(), err
}
return r.OrganizationID, nil
case influxdb.ScraperResourceType:
r, err := s.GetTargetByID(ctx, id)
if err != nil {
return influxdb.InvalidID(), err
}
return r.OrgID, nil
}
return influxdb.InvalidID(), &influxdb.Error{
Msg: fmt.Sprintf("unsupported resource type %s", rt),
}
}
// OrgAlreadyExistsError is used when creating a new organization with
// a name that has already been used. Organization names must be unique.
func OrgAlreadyExistsError(o *influxdb.Organization) error {
return &influxdb.Error{
Code: influxdb.EConflict,
Msg: fmt.Sprintf("organization with name %s already exists", o.Name),
}
}

68
kv/org_test.go Normal file
View File

@ -0,0 +1,68 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltOrganizationService(t *testing.T) {
influxdbtesting.OrganizationService(initBoltOrganizationService, t)
}
func TestInmemOrganizationService(t *testing.T) {
influxdbtesting.OrganizationService(initInmemOrganizationService, t)
}
func initBoltOrganizationService(f influxdbtesting.OrganizationFields, t *testing.T) (influxdb.OrganizationService, string, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initOrganizationService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initInmemOrganizationService(f influxdbtesting.OrganizationFields, t *testing.T) (influxdb.OrganizationService, string, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initOrganizationService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initOrganizationService(s kv.Store, f influxdbtesting.OrganizationFields, t *testing.T) (influxdb.OrganizationService, string, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing organization service: %v", err)
}
for _, u := range f.Organizations {
if err := svc.PutOrganization(ctx, u); err != nil {
t.Fatalf("failed to populate organizations")
}
}
return svc, kv.OpPrefix, func() {
for _, u := range f.Organizations {
if err := svc.DeleteOrganization(ctx, u.ID); err != nil {
t.Logf("failed to remove organizations: %v", err)
}
}
}
}

200
kv/passwords.go Normal file
View File

@ -0,0 +1,200 @@
package kv
import (
"context"
"fmt"
"github.com/influxdata/influxdb"
"golang.org/x/crypto/bcrypt"
)
// MinPasswordLength is the shortest password we allow into the system.
const MinPasswordLength = 8
var (
// EIncorrectPassword is returned when any password operation fails in which
// we do not want to leak information.
EIncorrectPassword = &influxdb.Error{
Code: influxdb.EForbidden,
Msg: "your username or password is incorrect",
}
// EShortPassword is used when a password is less than the minimum
// acceptable password length.
EShortPassword = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "passwords are required to be longer than 8 characters",
}
)
// UnavailablePasswordServiceError is used if we aren't able to add the
// password to the store, it means the store is not available at the moment
// (e.g. network).
func UnavailablePasswordServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EUnavailable,
Msg: fmt.Sprintf("Unable to connect to password service. Please try again; Err: %v", err),
Op: "kv/setPassword",
}
}
// CorruptUserIDError is used when the ID was encoded incorrectly previously.
// This is some sort of internal server error.
func CorruptUserIDError(name string, err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("User ID for %s has been corrupted; Err: %v", name, err),
Op: "kv/setPassword",
}
}
// InternalPasswordHashError is used if the hasher is unable to generate
// a hash of the password. This is some sort of internal server error.
func InternalPasswordHashError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unable to generate password; Err: %v", err),
Op: "kv/setPassword",
}
}
var (
userpasswordBucket = []byte("userspasswordv1")
)
var _ influxdb.PasswordsService = (*Service)(nil)
func (s *Service) initializePasswords(ctx context.Context, tx Tx) error {
_, err := tx.Bucket(userpasswordBucket)
return err
}
// CompareAndSetPassword checks the password and if they match
// updates to the new password.
func (s *Service) CompareAndSetPassword(ctx context.Context, name string, old string, new string) error {
return s.kv.Update(func(tx Tx) error {
if err := s.comparePassword(ctx, tx, name, old); err != nil {
return err
}
return s.setPassword(ctx, tx, name, new)
})
}
// SetPassword overrides the password of a known user.
func (s *Service) SetPassword(ctx context.Context, name string, password string) error {
return s.kv.Update(func(tx Tx) error {
return s.setPassword(ctx, tx, name, password)
})
}
// ComparePassword checks if the password matches the password recorded.
// Passwords that do not match return errors.
func (s *Service) ComparePassword(ctx context.Context, name string, password string) error {
return s.kv.View(func(tx Tx) error {
return s.comparePassword(ctx, tx, name, password)
})
}
func (s *Service) setPassword(ctx context.Context, tx Tx, name string, password string) error {
if len(password) < MinPasswordLength {
return EShortPassword
}
u, err := s.findUserByName(ctx, tx, name)
if err != nil {
return EIncorrectPassword
}
encodedID, err := u.ID.Encode()
if err != nil {
return CorruptUserIDError(name, err)
}
b, err := tx.Bucket(userpasswordBucket)
if err != nil {
return UnavailablePasswordServiceError(err)
}
hasher := s.Hash
if hasher == nil {
hasher = &Bcrypt{}
}
hash, err := hasher.GenerateFromPassword([]byte(password), DefaultCost)
if err != nil {
return InternalPasswordHashError(err)
}
if err := b.Put(encodedID, hash); err != nil {
return UnavailablePasswordServiceError(err)
}
return nil
}
func (s *Service) comparePassword(ctx context.Context, tx Tx, name string, password string) error {
u, err := s.findUserByName(ctx, tx, name)
if err != nil {
return EIncorrectPassword
}
encodedID, err := u.ID.Encode()
if err != nil {
return CorruptUserIDError(name, err)
}
b, err := tx.Bucket(userpasswordBucket)
if err != nil {
return UnavailablePasswordServiceError(err)
}
hash, err := b.Get(encodedID)
if err != nil {
// User exists but has no password has been set.
return EIncorrectPassword
}
hasher := s.Hash
if hasher == nil {
hasher = &Bcrypt{}
}
if err := hasher.CompareHashAndPassword(hash, []byte(password)); err != nil {
// User exists but the password was incorrect
return EIncorrectPassword
}
return nil
}
// DefaultCost is the cost that will actually be set if a cost below MinCost
// is passed into GenerateFromPassword
var DefaultCost = bcrypt.DefaultCost
// Crypt represents a cryptographic hashing function.
type Crypt interface {
// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
// Returns nil on success, or an error on failure.
CompareHashAndPassword(hashedPassword, password []byte) error
// GenerateFromPassword returns the hash of the password at the given cost.
// If the cost given is less than MinCost, the cost will be set to DefaultCost, instead.
GenerateFromPassword(password []byte, cost int) ([]byte, error)
}
var _ Crypt = (*Bcrypt)(nil)
// Bcrypt implements Crypt using golang.org/x/crypto/bcrypt
type Bcrypt struct{}
// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
// Returns nil on success, or an error on failure.
func (b *Bcrypt) CompareHashAndPassword(hashedPassword, password []byte) error {
return bcrypt.CompareHashAndPassword(hashedPassword, password)
}
// GenerateFromPassword returns the hash of the password at the given cost.
// If the cost given is less than MinCost, the cost will be set to DefaultCost, instead.
func (b *Bcrypt) GenerateFromPassword(password []byte, cost int) ([]byte, error) {
if cost < bcrypt.MinCost {
cost = DefaultCost
}
return bcrypt.GenerateFromPassword(password, cost)
}

507
kv/passwords_test.go Normal file
View File

@ -0,0 +1,507 @@
package kv_test
import (
"context"
"fmt"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
"github.com/influxdata/influxdb/mock"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltPasswordService(t *testing.T) {
influxdbtesting.PasswordsService(initBoltPasswordsService, t)
}
func TestInmemPasswordService(t *testing.T) {
influxdbtesting.PasswordsService(initInmemPasswordsService, t)
}
func initBoltPasswordsService(f influxdbtesting.PasswordFields, t *testing.T) (influxdb.PasswordsService, func()) {
s, closeStore, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new bolt kv store: %v", err)
}
svc, closeSvc := initPasswordsService(s, f, t)
return svc, func() {
closeSvc()
closeStore()
}
}
func initInmemPasswordsService(f influxdbtesting.PasswordFields, t *testing.T) (influxdb.PasswordsService, func()) {
s, closeStore, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new inmem kv store: %v", err)
}
svc, closeSvc := initPasswordsService(s, f, t)
return svc, func() {
closeSvc()
closeStore()
}
}
func initPasswordsService(s kv.Store, f influxdbtesting.PasswordFields, t *testing.T) (influxdb.PasswordsService, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing authorization service: %v", err)
}
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("error populating users: %v", err)
}
}
for i := range f.Passwords {
if err := svc.SetPassword(ctx, f.Users[i].Name, f.Passwords[i]); err != nil {
t.Fatalf("error setting passsword user, %s %s: %v", f.Users[i].Name, f.Passwords[i], err)
}
}
return svc, func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("error removing users: %v", err)
}
}
}
}
type MockHasher struct {
GenerateError error
CompareError error
}
func (m *MockHasher) CompareHashAndPassword(hashedPassword, password []byte) error {
return m.CompareError
}
func (m *MockHasher) GenerateFromPassword(password []byte, cost int) ([]byte, error) {
return nil, m.GenerateError
}
func TestService_SetPassword(t *testing.T) {
type fields struct {
kv kv.Store
Hash kv.Crypt
}
type args struct {
name string
password string
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "if store somehow has a corrupted user index, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
return nil, nil
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("<forbidden> your username or password is incorrect"),
},
},
{
name: "if user id is not found return a generic sounding error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("<forbidden> your username or password is incorrect"),
},
},
{
name: "if store somehow has a corrupted user id, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("kv/setPassword: <internal error> User ID for user1 has been corrupted; Err: invalid ID"),
},
},
{
name: "if password store is not available, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return nil, fmt.Errorf("internal bucket error")
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("kv/setPassword: <unavailable> Unable to connect to password service. Please try again; Err: internal bucket error"),
},
},
{
name: "if hashing algorithm has an error, then, we get back an internal error",
fields: fields{
Hash: &MockHasher{
GenerateError: fmt.Errorf("generate error"),
},
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return nil, nil
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
fmt.Errorf("kv/setPassword: <internal error> Unable to generate password; Err: generate error"),
},
},
{
name: "if not able to store the hashed password should have an internal error",
fields: fields{
kv: &mock.Store{
UpdateFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return &mock.Bucket{
PutFn: func(key, value []byte) error {
return fmt.Errorf("internal error")
},
}, nil
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
fmt.Errorf("kv/setPassword: <unavailable> Unable to connect to password service. Please try again; Err: internal error"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &kv.Service{
Hash: tt.fields.Hash,
}
s.WithStore(tt.fields.kv)
err := s.SetPassword(context.Background(), tt.args.name, tt.args.password)
if (err != nil && tt.wants.err == nil) || (err == nil && tt.wants.err != nil) {
t.Fatalf("Service.SetPassword() error = %v, want %v", err, tt.wants.err)
return
}
if err != nil {
if got, want := err.Error(), tt.wants.err.Error(); got != want {
t.Errorf("Service.SetPassword() error = %v, want %v", got, want)
}
}
})
}
}
func TestService_ComparePassword(t *testing.T) {
type fields struct {
kv kv.Store
Hash kv.Crypt
}
type args struct {
name string
password string
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "if store somehow has a corrupted user index, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
ViewFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
return nil, nil
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("<forbidden> your username or password is incorrect"),
},
},
{
name: "if store somehow has a corrupted user id, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
ViewFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("kv/setPassword: <internal error> User ID for user1 has been corrupted; Err: invalid ID"),
},
},
{
name: "if password store is not available, then, we get back an internal error",
fields: fields{
kv: &mock.Store{
ViewFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return nil, fmt.Errorf("internal bucket error")
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("kv/setPassword: <unavailable> Unable to connect to password service. Please try again; Err: internal bucket error"),
},
},
{
name: "if the password doesn't has correctly we get an invalid password error",
fields: fields{
Hash: &MockHasher{
CompareError: fmt.Errorf("generate error"),
},
kv: &mock.Store{
ViewFn: func(fn func(kv.Tx) error) error {
tx := &mock.Tx{
BucketFn: func(b []byte) (kv.Bucket, error) {
if string(b) == "userspasswordv1" {
return &mock.Bucket{
GetFn: func([]byte) ([]byte, error) {
return []byte("hash"), nil
},
}, nil
}
return &mock.Bucket{
GetFn: func(key []byte) ([]byte, error) {
if string(key) == "user1" {
return []byte("0000000000000001"), nil
}
if string(key) == "0000000000000001" {
return []byte(`{"id": "0000000000000001", "name": "user1"}`), nil
}
return nil, kv.ErrKeyNotFound
},
}, nil
},
}
return fn(tx)
},
},
},
args: args{
name: "user1",
password: "howdydoody",
},
wants: wants{
fmt.Errorf("<forbidden> your username or password is incorrect"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &kv.Service{
Hash: tt.fields.Hash,
}
s.WithStore(tt.fields.kv)
err := s.ComparePassword(context.Background(), tt.args.name, tt.args.password)
if (err != nil && tt.wants.err == nil) || (err == nil && tt.wants.err != nil) {
t.Fatalf("Service.ComparePassword() error = %v, want %v", err, tt.wants.err)
return
}
if err != nil {
if got, want := err.Error(), tt.wants.err.Error(); got != want {
t.Errorf("Service.ComparePassword() error = %v, want %v", got, want)
}
}
})
}
}

318
kv/scrapers.go Normal file
View File

@ -0,0 +1,318 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"github.com/influxdata/influxdb"
)
var (
// ErrScraperNotFound is used when the scraper configuration is not found.
ErrScraperNotFound = &influxdb.Error{
Msg: "scraper target is not found",
Code: influxdb.ENotFound,
}
// ErrInvalidScraperID is used when the service was provided
// an invalid ID format.
ErrInvalidScraperID = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "provided scraper target ID has invalid format",
}
// ErrInvalidScrapersBucketID is used when the service was provided
// an invalid ID format.
ErrInvalidScrapersBucketID = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "provided bucket ID has invalid format",
}
// ErrInvalidScrapersOrgID is used when the service was provided
// an invalid ID format.
ErrInvalidScrapersOrgID = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "provided organization ID has invalid format",
}
)
// UnexpectedScrapersBucketError is used when the error comes from an internal system.
func UnexpectedScrapersBucketError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: "unexpected error retrieving scrapers bucket",
Err: err,
Op: "kv/scraper",
}
}
// CorruptScraperError is used when the config cannot be unmarshalled from the
// bytes stored in the kv.
func CorruptScraperError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unknown internal scraper data error; Err: %v", err),
Op: "kv/scraper",
}
}
// ErrUnprocessableScraper is used when a scraper is not able to be converted to JSON.
func ErrUnprocessableScraper(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: fmt.Sprintf("unable to convert scraper target into JSON; Err %v", err),
}
}
// InternalScraperServiceError is used when the error comes from an
// internal system.
func InternalScraperServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unknown internal scraper data error; Err: %v", err),
Op: "kv/scraper",
}
}
var (
scrapersBucket = []byte("scraperv2")
)
var _ influxdb.ScraperTargetStoreService = (*Service)(nil)
func (s *Service) initializeScraperTargets(ctx context.Context, tx Tx) error {
_, err := s.scrapersBucket(tx)
return err
}
func (s *Service) scrapersBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket([]byte(scrapersBucket))
if err != nil {
return nil, UnexpectedScrapersBucketError(err)
}
return b, nil
}
// ListTargets will list all scrape targets.
func (s *Service) ListTargets(ctx context.Context) ([]influxdb.ScraperTarget, error) {
targets := []influxdb.ScraperTarget{}
err := s.kv.View(func(tx Tx) error {
var err error
targets, err = s.listTargets(ctx, tx)
return err
})
return targets, err
}
func (s *Service) listTargets(ctx context.Context, tx Tx) ([]influxdb.ScraperTarget, error) {
bucket, err := s.scrapersBucket(tx)
if err != nil {
return nil, err
}
cur, err := bucket.Cursor()
if err != nil {
return nil, UnexpectedScrapersBucketError(err)
}
targets := []influxdb.ScraperTarget{}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
target, err := unmarshalScraper(v)
if err != nil {
return nil, err
}
targets = append(targets, *target)
}
return targets, nil
}
// AddTarget add a new scraper target into storage.
func (s *Service) AddTarget(ctx context.Context, target *influxdb.ScraperTarget, userID influxdb.ID) (err error) {
return s.kv.Update(func(tx Tx) error {
return s.addTarget(ctx, tx, target, userID)
})
}
func (s *Service) addTarget(ctx context.Context, tx Tx, target *influxdb.ScraperTarget, userID influxdb.ID) error {
if !target.OrgID.Valid() {
return ErrInvalidScrapersOrgID
}
if !target.BucketID.Valid() {
return ErrInvalidScrapersBucketID
}
target.ID = s.IDGenerator.ID()
if err := s.putTarget(ctx, tx, target); err != nil {
return err
}
urm := &influxdb.UserResourceMapping{
ResourceID: target.ID,
UserID: userID,
UserType: influxdb.Owner,
ResourceType: influxdb.ScraperResourceType,
}
return s.createUserResourceMapping(ctx, tx, urm)
}
// RemoveTarget removes a scraper target from the bucket.
func (s *Service) RemoveTarget(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
return s.removeTarget(ctx, tx, id)
})
}
func (s *Service) removeTarget(ctx context.Context, tx Tx, id influxdb.ID) error {
_, pe := s.findTargetByID(ctx, tx, id)
if pe != nil {
return pe
}
encID, err := id.Encode()
if err != nil {
return ErrInvalidScraperID
}
bucket, err := s.scrapersBucket(tx)
if err != nil {
return err
}
_, err = bucket.Get(encID)
if IsNotFound(err) {
return ErrScraperNotFound
}
if err != nil {
return InternalScraperServiceError(err)
}
if err := bucket.Delete(encID); err != nil {
return InternalScraperServiceError(err)
}
return s.deleteUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: id,
ResourceType: influxdb.ScraperResourceType,
})
}
// UpdateTarget updates a scraper target.
func (s *Service) UpdateTarget(ctx context.Context, update *influxdb.ScraperTarget, userID influxdb.ID) (*influxdb.ScraperTarget, error) {
var target *influxdb.ScraperTarget
err := s.kv.Update(func(tx Tx) error {
var err error
target, err = s.updateTarget(ctx, tx, update, userID)
return err
})
return target, err
}
func (s *Service) updateTarget(ctx context.Context, tx Tx, update *influxdb.ScraperTarget, userID influxdb.ID) (*influxdb.ScraperTarget, error) {
if !update.ID.Valid() {
return nil, ErrInvalidScraperID
}
target, err := s.findTargetByID(ctx, tx, update.ID)
if err != nil {
return nil, err
}
// If the bucket or org are invalid, just use the ids from the original.
if !update.BucketID.Valid() {
update.BucketID = target.BucketID
}
if !update.OrgID.Valid() {
update.OrgID = target.OrgID
}
target = update
return target, s.putTarget(ctx, tx, target)
}
// GetTargetByID retrieves a scraper target by id.
func (s *Service) GetTargetByID(ctx context.Context, id influxdb.ID) (*influxdb.ScraperTarget, error) {
var target *influxdb.ScraperTarget
err := s.kv.View(func(tx Tx) error {
var err error
target, err = s.findTargetByID(ctx, tx, id)
return err
})
return target, err
}
func (s *Service) findTargetByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.ScraperTarget, error) {
encID, err := id.Encode()
if err != nil {
return nil, ErrInvalidScraperID
}
bucket, err := s.scrapersBucket(tx)
if err != nil {
return nil, err
}
v, err := bucket.Get(encID)
if IsNotFound(err) {
return nil, ErrScraperNotFound
}
if err != nil {
return nil, InternalScraperServiceError(err)
}
target, err := unmarshalScraper(v)
if err != nil {
return nil, err
}
return target, nil
}
// PutTarget will put a scraper target without setting an ID.
func (s *Service) PutTarget(ctx context.Context, target *influxdb.ScraperTarget) error {
return s.kv.Update(func(tx Tx) error {
return s.putTarget(ctx, tx, target)
})
}
func (s *Service) putTarget(ctx context.Context, tx Tx, target *influxdb.ScraperTarget) error {
v, err := marshalScraper(target)
if err != nil {
return ErrUnprocessableScraper(err)
}
encID, err := target.ID.Encode()
if err != nil {
return ErrInvalidScraperID
}
bucket, err := s.scrapersBucket(tx)
if err != nil {
return err
}
if err := bucket.Put(encID, v); err != nil {
return UnexpectedScrapersBucketError(err)
}
return nil
}
// unmarshalScraper turns the stored byte slice in the kv into a *influxdb.ScraperTarget.
func unmarshalScraper(v []byte) (*influxdb.ScraperTarget, error) {
s := &influxdb.ScraperTarget{}
if err := json.Unmarshal(v, s); err != nil {
return nil, CorruptScraperError(err)
}
return s, nil
}
func marshalScraper(sc *influxdb.ScraperTarget) ([]byte, error) {
v, err := json.Marshal(sc)
if err != nil {
return nil, ErrUnprocessableScraper(err)
}
return v, nil
}

74
kv/scrapers_test.go Normal file
View File

@ -0,0 +1,74 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltScraperTargetStoreService(t *testing.T) {
influxdbtesting.ScraperService(initBoltTargetService, t)
}
func TestInmemScraperTargetStoreService(t *testing.T) {
influxdbtesting.ScraperService(initInmemTargetService, t)
}
func initBoltTargetService(f influxdbtesting.TargetFields, t *testing.T) (influxdb.ScraperTargetStoreService, string, func()) {
s, closeFn, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initScraperTargetStoreService(s, f, t)
return svc, op, func() {
closeSvc()
closeFn()
}
}
func initInmemTargetService(f influxdbtesting.TargetFields, t *testing.T) (influxdb.ScraperTargetStoreService, string, func()) {
s, closeFn, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initScraperTargetStoreService(s, f, t)
return svc, op, func() {
closeSvc()
closeFn()
}
}
func initScraperTargetStoreService(s kv.Store, f influxdbtesting.TargetFields, t *testing.T) (influxdb.ScraperTargetStoreService, string, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing user service: %v", err)
}
for _, target := range f.Targets {
if err := svc.PutTarget(ctx, target); err != nil {
t.Fatalf("failed to populate targets: %v", err)
}
}
for _, m := range f.UserResourceMappings {
if err := svc.CreateUserResourceMapping(ctx, m); err != nil {
t.Fatalf("failed to populate user resource mapping")
}
}
return svc, kv.OpPrefix, func() {
for _, target := range f.Targets {
if err := svc.RemoveTarget(ctx, target.ID); err != nil {
t.Logf("failed to remove targets: %v", err)
}
}
}
}

283
kv/secret.go Normal file
View File

@ -0,0 +1,283 @@
package kv
import (
"context"
"encoding/base64"
"errors"
"fmt"
"github.com/influxdata/influxdb"
)
var (
secretBucket = []byte("secretsv1")
)
var _ influxdb.SecretService = (*Service)(nil)
func (s *Service) initializeSecrets(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket(secretBucket); err != nil {
return err
}
return nil
}
// LoadSecret retrieves the secret value v found at key k for organization orgID.
func (s *Service) LoadSecret(ctx context.Context, orgID influxdb.ID, k string) (string, error) {
var v string
err := s.kv.View(func(tx Tx) error {
val, err := s.loadSecret(ctx, tx, orgID, k)
if err != nil {
return err
}
v = val
return nil
})
if err != nil {
return "", err
}
return v, nil
}
func (s *Service) loadSecret(ctx context.Context, tx Tx, orgID influxdb.ID, k string) (string, error) {
key, err := encodeSecretKey(orgID, k)
if err != nil {
return "", err
}
b, err := tx.Bucket(secretBucket)
if err != nil {
return "", err
}
val, err := b.Get(key)
if IsNotFound(err) {
return "", &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrSecretNotFound,
}
}
if err != nil {
return "", err
}
v, err := decodeSecretValue(val)
if err != nil {
return "", err
}
return v, nil
}
// GetSecretKeys retrieves all secret keys that are stored for the organization orgID.
func (s *Service) GetSecretKeys(ctx context.Context, orgID influxdb.ID) ([]string, error) {
var vs []string
err := s.kv.View(func(tx Tx) error {
vals, err := s.getSecretKeys(ctx, tx, orgID)
if err != nil {
return err
}
vs = vals
return nil
})
if err != nil {
return nil, err
}
return vs, nil
}
func (s *Service) getSecretKeys(ctx context.Context, tx Tx, orgID influxdb.ID) ([]string, error) {
b, err := tx.Bucket(secretBucket)
if err != nil {
return nil, err
}
cur, err := b.Cursor()
if err != nil {
return nil, err
}
prefix, err := orgID.Encode()
if err != nil {
return nil, err
}
k, _ := cur.Seek(prefix)
if len(k) == 0 {
return []string{}, nil
}
id, key, err := decodeSecretKey(k)
if err != nil {
return nil, err
}
if id != orgID {
return nil, fmt.Errorf("organization has no secret keys")
}
keys := []string{key}
for {
k, _ = cur.Next()
if len(k) == 0 {
// We've reached the end of the keys so we're done
break
}
id, key, err = decodeSecretKey(k)
if err != nil {
return nil, err
}
if id != orgID {
// We've reached the end of the keyspace for the provided orgID
break
}
keys = append(keys, key)
}
return keys, nil
}
// PutSecret stores the secret pair (k,v) for the organization orgID.
func (s *Service) PutSecret(ctx context.Context, orgID influxdb.ID, k, v string) error {
return s.kv.Update(func(tx Tx) error {
return s.putSecret(ctx, tx, orgID, k, v)
})
}
func (s *Service) putSecret(ctx context.Context, tx Tx, orgID influxdb.ID, k, v string) error {
key, err := encodeSecretKey(orgID, k)
if err != nil {
return err
}
val := encodeSecretValue(v)
b, err := tx.Bucket(secretBucket)
if err != nil {
return err
}
if err := b.Put(key, val); err != nil {
return err
}
return nil
}
func encodeSecretKey(orgID influxdb.ID, k string) ([]byte, error) {
buf, err := orgID.Encode()
if err != nil {
return nil, err
}
key := make([]byte, 0, influxdb.IDLength+len(k))
key = append(key, buf...)
key = append(key, k...)
return key, nil
}
func decodeSecretKey(key []byte) (influxdb.ID, string, error) {
if len(key) < influxdb.IDLength {
// This should not happen.
return influxdb.InvalidID(), "", errors.New("provided key is too short to contain an ID (please report this error)")
}
var id influxdb.ID
if err := id.Decode(key[:influxdb.IDLength]); err != nil {
return influxdb.InvalidID(), "", err
}
k := string(key[influxdb.IDLength:])
return id, k, nil
}
func decodeSecretValue(val []byte) (string, error) {
// store the secret value base64 encoded so that it's marginally better than plaintext
v := make([]byte, base64.StdEncoding.DecodedLen(len(val)))
if _, err := base64.StdEncoding.Decode(v, val); err != nil {
return "", err
}
return string(v), nil
}
func encodeSecretValue(v string) []byte {
val := make([]byte, base64.StdEncoding.EncodedLen(len(v)))
base64.StdEncoding.Encode(val, []byte(v))
return val
}
// PutSecrets puts all provided secrets and overwrites any previous values.
func (s *Service) PutSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return s.kv.Update(func(tx Tx) error {
keys, err := s.getSecretKeys(ctx, tx, orgID)
if err != nil {
return err
}
for k, v := range m {
if err := s.putSecret(ctx, tx, orgID, k, v); err != nil {
return err
}
}
for _, k := range keys {
if _, ok := m[k]; !ok {
if err := s.deleteSecret(ctx, tx, orgID, k); err != nil {
return err
}
}
}
return nil
})
}
// PatchSecrets patches all provided secrets and updates any previous values.
func (s *Service) PatchSecrets(ctx context.Context, orgID influxdb.ID, m map[string]string) error {
return s.kv.Update(func(tx Tx) error {
for k, v := range m {
if err := s.putSecret(ctx, tx, orgID, k, v); err != nil {
return err
}
}
return nil
})
}
// DeleteSecret removes secrets from the secret store.
func (s *Service) DeleteSecret(ctx context.Context, orgID influxdb.ID, ks ...string) error {
return s.kv.Update(func(tx Tx) error {
for _, k := range ks {
if err := s.deleteSecret(ctx, tx, orgID, k); err != nil {
return err
}
}
return nil
})
}
func (s *Service) deleteSecret(ctx context.Context, tx Tx, orgID influxdb.ID, k string) error {
key, err := encodeSecretKey(orgID, k)
if err != nil {
return err
}
b, err := tx.Bucket(secretBucket)
if err != nil {
return err
}
return b.Delete(key)
}

62
kv/secret_test.go Normal file
View File

@ -0,0 +1,62 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltSecretService(t *testing.T) {
influxdbtesting.SecretService(initBoltSecretService, t)
}
func TestInmemSecretService(t *testing.T) {
influxdbtesting.SecretService(initInmemSecretService, t)
}
func initBoltSecretService(f influxdbtesting.SecretServiceFields, t *testing.T) (influxdb.SecretService, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initSecretService(s, f, t)
return svc, func() {
closeSvc()
closeBolt()
}
}
func initInmemSecretService(f influxdbtesting.SecretServiceFields, t *testing.T) (influxdb.SecretService, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initSecretService(s, f, t)
return svc, func() {
closeSvc()
closeBolt()
}
}
func initSecretService(s kv.Store, f influxdbtesting.SecretServiceFields, t *testing.T) (influxdb.SecretService, func()) {
svc := kv.NewService(s)
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing secret service: %v", err)
}
for _, s := range f.Secrets {
for k, v := range s.Env {
if err := svc.PutSecret(ctx, s.OrganizationID, k, v); err != nil {
t.Fatalf("failed to populate secrets")
}
}
}
return svc, func() {}
}

121
kv/service.go Normal file
View File

@ -0,0 +1,121 @@
package kv
import (
"context"
"time"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/rand"
"github.com/influxdata/influxdb/snowflake"
"go.uber.org/zap"
)
var (
_ influxdb.UserService = (*Service)(nil)
)
// OpPrefix is the prefix for kv errors.
const OpPrefix = "kv/"
// Service is the struct that influxdb services are implemented on.
type Service struct {
kv Store
Logger *zap.Logger
IDGenerator influxdb.IDGenerator
TokenGenerator influxdb.TokenGenerator
Hash Crypt
time func() time.Time
}
// NewService returns an instance of a Service.
func NewService(kv Store) *Service {
return &Service{
Logger: zap.NewNop(),
IDGenerator: snowflake.NewIDGenerator(),
TokenGenerator: rand.NewTokenGenerator(64),
Hash: &Bcrypt{},
kv: kv,
time: time.Now,
}
}
// Initialize creates Buckets needed.
func (s *Service) Initialize(ctx context.Context) error {
return s.kv.Update(func(tx Tx) error {
if err := s.initializeAuths(ctx, tx); err != nil {
return err
}
if err := s.initializeBuckets(ctx, tx); err != nil {
return err
}
if err := s.initializeDashboards(ctx, tx); err != nil {
return err
}
if err := s.initializeKVLog(ctx, tx); err != nil {
return err
}
if err := s.initializeLabels(ctx, tx); err != nil {
return err
}
if err := s.initializeOnboarding(ctx, tx); err != nil {
return err
}
if err := s.initializeOrgs(ctx, tx); err != nil {
return err
}
if err := s.initializePasswords(ctx, tx); err != nil {
return err
}
if err := s.initializeScraperTargets(ctx, tx); err != nil {
return err
}
if err := s.initializeSecrets(ctx, tx); err != nil {
return err
}
if err := s.initializeSessions(ctx, tx); err != nil {
return err
}
if err := s.initializeSources(ctx, tx); err != nil {
return err
}
if err := s.initializeTelegraf(ctx, tx); err != nil {
return err
}
if err := s.initializeURMs(ctx, tx); err != nil {
return err
}
if err := s.initializeVariables(ctx, tx); err != nil {
return err
}
return s.initializeUsers(ctx, tx)
})
}
// WithTime sets the function for computing the current time. Used for updating meta data
// about objects stored. Should only be used in tests for mocking.
func (s *Service) WithTime(fn func() time.Time) {
s.time = fn
}
// WithStore sets kv store for the service.
// Should only be used in tests for mocking.
func (s *Service) WithStore(store Store) {
s.kv = store
}

216
kv/session.go Normal file
View File

@ -0,0 +1,216 @@
package kv
import (
"context"
"encoding/json"
"time"
"github.com/influxdata/influxdb"
)
var (
sessionBucket = []byte("sessionsv1")
)
var _ influxdb.SessionService = (*Service)(nil)
func (s *Service) initializeSessions(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket([]byte(sessionBucket)); err != nil {
return err
}
return nil
}
// RenewSession extends the expire time to newExpiration.
func (s *Service) RenewSession(ctx context.Context, session *influxdb.Session, newExpiration time.Time) error {
if session == nil {
return &influxdb.Error{
Msg: "session is nil",
}
}
return s.kv.Update(func(tx Tx) error {
session.ExpiresAt = newExpiration
if err := s.putSession(ctx, tx, session); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
})
}
// FindSession retrieves the session found at the provided key.
func (s *Service) FindSession(ctx context.Context, key string) (*influxdb.Session, error) {
var sess *influxdb.Session
err := s.kv.View(func(tx Tx) error {
s, err := s.findSession(ctx, tx, key)
if err != nil {
return err
}
sess = s
return nil
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
if err := sess.Expired(); err != nil {
// todo(leodido) > do we want to return session also if expired?
return sess, &influxdb.Error{
Err: err,
}
}
return sess, nil
}
func (s *Service) findSession(ctx context.Context, tx Tx, key string) (*influxdb.Session, error) {
b, err := tx.Bucket(sessionBucket)
if err != nil {
return nil, err
}
v, err := b.Get([]byte(key))
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrSessionNotFound,
}
}
if err != nil {
return nil, err
}
sn := &influxdb.Session{}
if err := json.Unmarshal(v, sn); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
// TODO(desa): these values should be cached so it's not so expensive to lookup each time.
f := influxdb.UserResourceMappingFilter{UserID: sn.UserID}
mappings, err := s.findUserResourceMappings(ctx, tx, f)
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
ps := make([]influxdb.Permission, 0, len(mappings))
for _, m := range mappings {
p, err := m.ToPermissions()
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
ps = append(ps, p...)
}
ps = append(ps, influxdb.MePermissions(sn.UserID)...)
sn.Permissions = ps
return sn, nil
}
// PutSession puts the session at key.
func (s *Service) PutSession(ctx context.Context, sn *influxdb.Session) error {
return s.kv.Update(func(tx Tx) error {
if err := s.putSession(ctx, tx, sn); err != nil {
return err
}
return nil
})
}
func (s *Service) putSession(ctx context.Context, tx Tx, sn *influxdb.Session) error {
v, err := json.Marshal(sn)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(sessionBucket)
if err != nil {
return err
}
if err := b.Put([]byte(sn.Key), v); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// ExpireSession expires the session at the provided key.
func (s *Service) ExpireSession(ctx context.Context, key string) error {
return s.kv.Update(func(tx Tx) error {
sn, err := s.findSession(ctx, tx, key)
if err != nil {
return err
}
sn.ExpiresAt = time.Now()
if err := s.putSession(ctx, tx, sn); err != nil {
return err
}
return nil
})
}
// CreateSession creates a session for a user with the users maximal privileges.
func (s *Service) CreateSession(ctx context.Context, user string) (*influxdb.Session, error) {
var sess *influxdb.Session
err := s.kv.Update(func(tx Tx) error {
sn, err := s.createSession(ctx, tx, user)
if err != nil {
return err
}
sess = sn
return nil
})
if err != nil {
return nil, err
}
return sess, nil
}
func (s *Service) createSession(ctx context.Context, tx Tx, user string) (*influxdb.Session, error) {
u, pe := s.findUserByName(ctx, tx, user)
if pe != nil {
return nil, pe
}
sn := &influxdb.Session{}
sn.ID = s.IDGenerator.ID()
k, err := s.TokenGenerator.Token()
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
sn.Key = k
sn.UserID = u.ID
sn.CreatedAt = time.Now()
// TODO(desa): make this configurable
sn.ExpiresAt = sn.CreatedAt.Add(time.Hour)
// TODO(desa): not totally sure what to do here. Possibly we should have a maximal privilege permission.
sn.Permissions = []influxdb.Permission{}
if err := s.putSession(ctx, tx, sn); err != nil {
return nil, err
}
return sn, nil
}

73
kv/session_test.go Normal file
View File

@ -0,0 +1,73 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltSessionService(t *testing.T) {
influxdbtesting.SessionService(initBoltSessionService, t)
}
func TestInmemSessionService(t *testing.T) {
influxdbtesting.SessionService(initInmemSessionService, t)
}
func initBoltSessionService(f influxdbtesting.SessionFields, t *testing.T) (influxdb.SessionService, string, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initSessionService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initInmemSessionService(f influxdbtesting.SessionFields, t *testing.T) (influxdb.SessionService, string, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initSessionService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initSessionService(s kv.Store, f influxdbtesting.SessionFields, t *testing.T) (influxdb.SessionService, string, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
svc.TokenGenerator = f.TokenGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing session service: %v", err)
}
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
for _, s := range f.Sessions {
if err := svc.PutSession(ctx, s); err != nil {
t.Fatalf("failed to populate sessions")
}
}
return svc, kv.OpPrefix, func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove users: %v", err)
}
}
}
}

336
kv/source.go Normal file
View File

@ -0,0 +1,336 @@
package kv
import (
"context"
"encoding/json"
"fmt"
influxdb "github.com/influxdata/influxdb"
)
var (
sourceBucket = []byte("sourcesv1")
)
// DefaultSource is the default source.
var DefaultSource = influxdb.Source{
Default: true,
Name: "autogen",
Type: influxdb.SelfSourceType,
}
const (
// DefaultSourceID it the default source identifier
DefaultSourceID = "020f755c3c082000"
// DefaultSourceOrganizationID is the default source's organization identifier
DefaultSourceOrganizationID = "50616e67652c206c"
)
func init() {
if err := DefaultSource.ID.DecodeFromString(DefaultSourceID); err != nil {
panic(fmt.Sprintf("failed to decode default source id: %v", err))
}
if err := DefaultSource.OrganizationID.DecodeFromString(DefaultSourceOrganizationID); err != nil {
panic(fmt.Sprintf("failed to decode default source organization id: %v", err))
}
}
func (s *Service) initializeSources(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket(sourceBucket); err != nil {
return err
}
_, pe := s.findSourceByID(ctx, tx, DefaultSource.ID)
if pe != nil && influxdb.ErrorCode(pe) != influxdb.ENotFound {
return pe
}
if influxdb.ErrorCode(pe) == influxdb.ENotFound {
if err := s.putSource(ctx, tx, &DefaultSource); err != nil {
return err
}
}
return nil
}
// DefaultSource retrieves the default source.
func (s *Service) DefaultSource(ctx context.Context) (*influxdb.Source, error) {
var sr *influxdb.Source
err := s.kv.View(func(tx Tx) error {
// TODO(desa): make this faster by putting the default source in an index.
srcs, err := s.findSources(ctx, tx, influxdb.FindOptions{})
if err != nil {
return err
}
for _, src := range srcs {
if src.Default {
sr = src
return nil
}
}
return &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "no default source found",
}
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return sr, nil
}
// FindSourceByID retrieves a source by id.
func (s *Service) FindSourceByID(ctx context.Context, id influxdb.ID) (*influxdb.Source, error) {
var sr *influxdb.Source
err := s.kv.View(func(tx Tx) error {
src, pe := s.findSourceByID(ctx, tx, id)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
sr = src
return nil
})
return sr, err
}
func (s *Service) findSourceByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Source, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(sourceBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrSourceNotFound,
}
}
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
var sr influxdb.Source
if err := json.Unmarshal(v, &sr); err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return &sr, nil
}
// FindSources retrives all sources that match an arbitrary source filter.
// Filters using ID, or OrganizationID and source Name should be efficient.
// Other filters will do a linear scan across all sources searching for a match.
func (s *Service) FindSources(ctx context.Context, opt influxdb.FindOptions) ([]*influxdb.Source, int, error) {
ss := []*influxdb.Source{}
err := s.kv.View(func(tx Tx) error {
srcs, err := s.findSources(ctx, tx, opt)
if err != nil {
return err
}
ss = srcs
return nil
})
if err != nil {
return nil, 0, &influxdb.Error{
Op: influxdb.OpFindSources,
Err: err,
}
}
return ss, len(ss), nil
}
func (s *Service) findSources(ctx context.Context, tx Tx, opt influxdb.FindOptions) ([]*influxdb.Source, error) {
ss := []*influxdb.Source{}
err := s.forEachSource(ctx, tx, func(s *influxdb.Source) bool {
ss = append(ss, s)
return true
})
if err != nil {
return nil, err
}
return ss, nil
}
// CreateSource creates a influxdb source and sets s.ID.
func (s *Service) CreateSource(ctx context.Context, src *influxdb.Source) error {
err := s.kv.Update(func(tx Tx) error {
src.ID = s.IDGenerator.ID()
// Generating an organization id if it missing or invalid
if !src.OrganizationID.Valid() {
src.OrganizationID = s.IDGenerator.ID()
}
return s.putSource(ctx, tx, src)
})
if err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// PutSource will put a source without setting an ID.
func (s *Service) PutSource(ctx context.Context, src *influxdb.Source) error {
return s.kv.Update(func(tx Tx) error {
return s.putSource(ctx, tx, src)
})
}
func (s *Service) putSource(ctx context.Context, tx Tx, src *influxdb.Source) error {
v, err := json.Marshal(src)
if err != nil {
return err
}
encodedID, err := src.ID.Encode()
if err != nil {
return err
}
b, err := tx.Bucket(sourceBucket)
if err != nil {
return err
}
if err := b.Put(encodedID, v); err != nil {
return err
}
return nil
}
// forEachSource will iterate through all sources while fn returns true.
func (s *Service) forEachSource(ctx context.Context, tx Tx, fn func(*influxdb.Source) bool) error {
b, err := tx.Bucket(sourceBucket)
if err != nil {
return err
}
cur, err := b.Cursor()
if err != nil {
return err
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
s := &influxdb.Source{}
if err := json.Unmarshal(v, s); err != nil {
return err
}
if !fn(s) {
break
}
}
return nil
}
// UpdateSource updates a source according the parameters set on upd.
func (s *Service) UpdateSource(ctx context.Context, id influxdb.ID, upd influxdb.SourceUpdate) (*influxdb.Source, error) {
var sr *influxdb.Source
err := s.kv.Update(func(tx Tx) error {
src, err := s.updateSource(ctx, tx, id, upd)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
sr = src
return nil
})
return sr, err
}
func (s *Service) updateSource(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.SourceUpdate) (*influxdb.Source, error) {
src, pe := s.findSourceByID(ctx, tx, id)
if pe != nil {
return nil, pe
}
if err := upd.Apply(src); err != nil {
return nil, err
}
if err := s.putSource(ctx, tx, src); err != nil {
return nil, err
}
return src, nil
}
// DeleteSource deletes a source and prunes it from the index.
func (s *Service) DeleteSource(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
pe := s.deleteSource(ctx, tx, id)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
return nil
})
}
func (s *Service) deleteSource(ctx context.Context, tx Tx, id influxdb.ID) error {
if id == DefaultSource.ID {
return &influxdb.Error{
Code: influxdb.EForbidden,
Msg: "cannot delete autogen source",
}
}
_, pe := s.findSourceByID(ctx, tx, id)
if pe != nil {
return pe
}
encodedID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(sourceBucket)
if err != nil {
return err
}
if err = b.Delete(encodedID); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}

72
kv/source_test.go Normal file
View File

@ -0,0 +1,72 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltSourceService(t *testing.T) {
t.Run("CreateSource", func(t *testing.T) { influxdbtesting.CreateSource(initBoltSourceService, t) })
t.Run("FindSourceByID", func(t *testing.T) { influxdbtesting.FindSourceByID(initBoltSourceService, t) })
t.Run("FindSources", func(t *testing.T) { influxdbtesting.FindSources(initBoltSourceService, t) })
t.Run("DeleteSource", func(t *testing.T) { influxdbtesting.DeleteSource(initBoltSourceService, t) })
}
func TestInmemSourceService(t *testing.T) {
t.Run("CreateSource", func(t *testing.T) { influxdbtesting.CreateSource(initInmemSourceService, t) })
t.Run("FindSourceByID", func(t *testing.T) { influxdbtesting.FindSourceByID(initInmemSourceService, t) })
t.Run("FindSources", func(t *testing.T) { influxdbtesting.FindSources(initInmemSourceService, t) })
t.Run("DeleteSource", func(t *testing.T) { influxdbtesting.DeleteSource(initInmemSourceService, t) })
}
func initBoltSourceService(f influxdbtesting.SourceFields, t *testing.T) (influxdb.SourceService, string, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initSourceService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initInmemSourceService(f influxdbtesting.SourceFields, t *testing.T) (influxdb.SourceService, string, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initSourceService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initSourceService(s kv.Store, f influxdbtesting.SourceFields, t *testing.T) (influxdb.SourceService, string, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing source service: %v", err)
}
for _, b := range f.Sources {
if err := svc.PutSource(ctx, b); err != nil {
t.Fatalf("failed to populate sources")
}
}
return svc, kv.OpPrefix, func() {
for _, b := range f.Sources {
if err := svc.DeleteSource(ctx, b.ID); err != nil {
t.Logf("failed to remove source: %v", err)
}
}
}
}

View File

@ -13,6 +13,11 @@ var (
ErrTxNotWritable = errors.New("transaction is not writable")
)
// IsNotFound returns a boolean indicating whether the error is known to report that a key or was not found.
func IsNotFound(err error) bool {
return err == ErrKeyNotFound
}
// Store is an interface for a generic key value store. It is modeled after
// the boltdb database struct.
type Store interface {
@ -25,15 +30,20 @@ type Store interface {
// Tx is a transaction in the store.
type Tx interface {
// Bucket possibly creates and returns bucket, b.
Bucket(b []byte) (Bucket, error)
// Context returns the context associated with this Tx.
Context() context.Context
// WithContext associates a context with this Tx.
WithContext(ctx context.Context)
}
// Bucket is the abstraction used to perform get/put/delete/get-many operations
// in a key value store.
type Bucket interface {
// Get returns a key within this bucket. Errors if key does not exist.
Get(key []byte) ([]byte, error)
// Cursor returns a cursor at the beginning of this bucket.
Cursor() (Cursor, error)
// Put should error if the transaction it was called in is not writable.
Put(key, value []byte) error
@ -44,9 +54,14 @@ type Bucket interface {
// Cursor is an abstraction for iterating/ranging through data. A concrete implementation
// of a cursor can be found in cursor.go.
type Cursor interface {
// Seek moves the cursor forward until reaching prefix in the key name.
Seek(prefix []byte) (k []byte, v []byte)
// First moves the cursor to the first key in the bucket.
First() (k []byte, v []byte)
// Last moves the cursor to the last key in the bucket.
Last() (k []byte, v []byte)
// Next moves the cursor to the next key in the bucket.
Next() (k []byte, v []byte)
// Prev moves the cursor to the prev key in the bucket.
Prev() (k []byte, v []byte)
}

298
kv/telegraf.go Normal file
View File

@ -0,0 +1,298 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"github.com/influxdata/influxdb"
)
var (
// ErrTelegrafNotFound is used when the telegraf configuration is not found.
ErrTelegrafNotFound = &influxdb.Error{
Msg: "telegraf configuration not found",
Code: influxdb.ENotFound,
}
// ErrInvalidTelegrafID is used when the service was provided
// an invalid ID format.
ErrInvalidTelegrafID = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "provided telegraf configuration ID has invalid format",
}
// ErrInvalidTelegrafOrgID is the error message for a missing or invalid organization ID.
ErrInvalidTelegrafOrgID = &influxdb.Error{
Code: influxdb.EEmptyValue,
Msg: "provided telegraf configuration organization ID is missing or invalid",
}
)
// UnavailableTelegrafServiceError is used if we aren't able to interact with the
// store, it means the store is not available at the moment (e.g. network).
func UnavailableTelegrafServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unable to connect to telegraf service. Please try again; Err: %v", err),
Op: "kv/telegraf",
}
}
// InternalTelegrafServiceError is used when the error comes from an
// internal system.
func InternalTelegrafServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unknown internal telegraf data error; Err: %v", err),
Op: "kv/telegraf",
}
}
// CorruptTelegrafError is used when the config cannot be unmarshalled from the
// bytes stored in the kv.
func CorruptTelegrafError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unknown internal telegraf data error; Err: %v", err),
Op: "kv/telegraf",
}
}
// ErrUnprocessableTelegraf is used when a telegraf is not able to be converted to JSON.
func ErrUnprocessableTelegraf(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: fmt.Sprintf("unable to convert telegraf configuration into JSON; Err %v", err),
}
}
var (
telegrafBucket = []byte("telegrafv1")
)
var _ influxdb.TelegrafConfigStore = (*Service)(nil)
func (s *Service) initializeTelegraf(ctx context.Context, tx Tx) error {
if _, err := s.telegrafBucket(tx); err != nil {
return err
}
return nil
}
func (s *Service) telegrafBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket(telegrafBucket)
if err != nil {
return nil, UnavailableTelegrafServiceError(err)
}
return b, nil
}
// FindTelegrafConfigByID returns a single telegraf config by ID.
func (s *Service) FindTelegrafConfigByID(ctx context.Context, id influxdb.ID) (*influxdb.TelegrafConfig, error) {
var (
tc *influxdb.TelegrafConfig
err error
)
err = s.kv.View(func(tx Tx) error {
tc, err = s.findTelegrafConfigByID(ctx, tx, id)
return err
})
return tc, err
}
func (s *Service) findTelegrafConfigByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.TelegrafConfig, error) {
encID, err := id.Encode()
if err != nil {
return nil, ErrInvalidTelegrafID
}
bucket, err := s.telegrafBucket(tx)
if err != nil {
return nil, err
}
v, err := bucket.Get(encID)
if IsNotFound(err) {
return nil, ErrTelegrafNotFound
}
if err != nil {
return nil, InternalTelegrafServiceError(err)
}
return unmarshalTelegraf(v)
}
// FindTelegrafConfigs returns a list of telegraf configs that match filter and the total count of matching telegraf configs.
// Additional options provide pagination & sorting.
func (s *Service) FindTelegrafConfigs(ctx context.Context, filter influxdb.TelegrafConfigFilter, opt ...influxdb.FindOptions) (tcs []*influxdb.TelegrafConfig, n int, err error) {
err = s.kv.View(func(tx Tx) error {
tcs, n, err = s.findTelegrafConfigs(ctx, tx, filter)
return err
})
return tcs, n, err
}
func (s *Service) findTelegrafConfigs(ctx context.Context, tx Tx, filter influxdb.TelegrafConfigFilter, opt ...influxdb.FindOptions) ([]*influxdb.TelegrafConfig, int, error) {
tcs := make([]*influxdb.TelegrafConfig, 0)
m, err := s.findUserResourceMappings(ctx, tx, filter.UserResourceMappingFilter)
if err != nil {
return nil, 0, err
}
if len(m) == 0 {
return tcs, 0, nil
}
for _, item := range m {
tc, err := s.findTelegrafConfigByID(ctx, tx, item.ResourceID)
if err == ErrTelegrafNotFound { // Stale user resource mappings are skipped
continue
}
if err != nil {
return nil, 0, InternalTelegrafServiceError(err)
}
// Restrict results by organization ID, if it has been provided
if filter.OrganizationID != nil && filter.OrganizationID.Valid() && tc.OrganizationID != *filter.OrganizationID {
continue
}
tcs = append(tcs, tc)
}
return tcs, len(tcs), nil
}
// PutTelegrafConfig put a telegraf config to storage.
func (s *Service) PutTelegrafConfig(ctx context.Context, tc *influxdb.TelegrafConfig) error {
return s.kv.Update(func(tx Tx) (err error) {
return s.putTelegrafConfig(ctx, tx, tc)
})
}
func (s *Service) putTelegrafConfig(ctx context.Context, tx Tx, tc *influxdb.TelegrafConfig) error {
encodedID, err := tc.ID.Encode()
if err != nil {
return ErrInvalidTelegrafID
}
if !tc.OrganizationID.Valid() {
return ErrInvalidTelegrafOrgID
}
v, err := marshalTelegraf(tc)
if err != nil {
return err
}
bucket, err := s.telegrafBucket(tx)
if err != nil {
return err
}
if err := bucket.Put(encodedID, v); err != nil {
return UnavailableTelegrafServiceError(err)
}
return nil
}
// CreateTelegrafConfig creates a new telegraf config and sets b.ID with the new identifier.
func (s *Service) CreateTelegrafConfig(ctx context.Context, tc *influxdb.TelegrafConfig, userID influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
return s.createTelegrafConfig(ctx, tx, tc, userID)
})
}
func (s *Service) createTelegrafConfig(ctx context.Context, tx Tx, tc *influxdb.TelegrafConfig, userID influxdb.ID) error {
tc.ID = s.IDGenerator.ID()
if err := s.putTelegrafConfig(ctx, tx, tc); err != nil {
return err
}
urm := &influxdb.UserResourceMapping{
ResourceID: tc.ID,
UserID: userID,
UserType: influxdb.Owner,
ResourceType: influxdb.TelegrafsResourceType,
}
return s.createUserResourceMapping(ctx, tx, urm)
}
// UpdateTelegrafConfig updates a single telegraf config.
// Returns the new telegraf config after update.
func (s *Service) UpdateTelegrafConfig(ctx context.Context, id influxdb.ID, tc *influxdb.TelegrafConfig, userID influxdb.ID) (*influxdb.TelegrafConfig, error) {
var err error
err = s.kv.Update(func(tx Tx) error {
tc, err = s.updateTelegrafConfig(ctx, tx, id, tc, userID)
return err
})
return tc, err
}
func (s *Service) updateTelegrafConfig(ctx context.Context, tx Tx, id influxdb.ID, tc *influxdb.TelegrafConfig, userID influxdb.ID) (*influxdb.TelegrafConfig, error) {
current, err := s.findTelegrafConfigByID(ctx, tx, id)
if err != nil {
return nil, err
}
// ID and OrganizationID can not be updated
tc.ID = current.ID
tc.OrganizationID = current.OrganizationID
err = s.putTelegrafConfig(ctx, tx, tc)
return tc, err
}
// DeleteTelegrafConfig removes a telegraf config by ID.
func (s *Service) DeleteTelegrafConfig(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
return s.deleteTelegrafConfig(ctx, tx, id)
})
}
func (s *Service) deleteTelegrafConfig(ctx context.Context, tx Tx, id influxdb.ID) error {
encodedID, err := id.Encode()
if err != nil {
return ErrInvalidTelegrafID
}
bucket, err := s.telegrafBucket(tx)
if err != nil {
return err
}
_, err = bucket.Get(encodedID)
if IsNotFound(err) {
return ErrTelegrafNotFound
}
if err != nil {
return InternalTelegrafServiceError(err)
}
if err := bucket.Delete(encodedID); err != nil {
return UnavailableTelegrafServiceError(err)
}
return s.deleteUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: id,
ResourceType: influxdb.TelegrafsResourceType,
})
}
// unmarshalTelegraf turns the stored byte slice in the kv into a *influxdb.TelegrafConfig.
func unmarshalTelegraf(v []byte) (*influxdb.TelegrafConfig, error) {
t := &influxdb.TelegrafConfig{}
if err := json.Unmarshal(v, t); err != nil {
return nil, CorruptTelegrafError(err)
}
return t, nil
}
func marshalTelegraf(tc *influxdb.TelegrafConfig) ([]byte, error) {
v, err := json.Marshal(tc)
if err != nil {
return nil, ErrUnprocessableTelegraf(err)
}
return v, nil
}

74
kv/telegraf_test.go Normal file
View File

@ -0,0 +1,74 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltTelegrafService(t *testing.T) {
influxdbtesting.TelegrafConfigStore(initBoltTelegrafService, t)
}
func TestInmemTelegrafService(t *testing.T) {
influxdbtesting.TelegrafConfigStore(initInmemTelegrafService, t)
}
func initBoltTelegrafService(f influxdbtesting.TelegrafConfigFields, t *testing.T) (influxdb.TelegrafConfigStore, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initTelegrafService(s, f, t)
return svc, func() {
closeSvc()
closeBolt()
}
}
func initInmemTelegrafService(f influxdbtesting.TelegrafConfigFields, t *testing.T) (influxdb.TelegrafConfigStore, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initTelegrafService(s, f, t)
return svc, func() {
closeSvc()
closeBolt()
}
}
func initTelegrafService(s kv.Store, f influxdbtesting.TelegrafConfigFields, t *testing.T) (influxdb.TelegrafConfigStore, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing user service: %v", err)
}
for _, tc := range f.TelegrafConfigs {
if err := svc.PutTelegrafConfig(ctx, tc); err != nil {
t.Fatalf("failed to populate telegraf config: %v", err)
}
}
for _, m := range f.UserResourceMappings {
if err := svc.CreateUserResourceMapping(ctx, m); err != nil {
t.Fatalf("failed to populate user resource mapping: %v", err)
}
}
return svc, func() {
for _, tc := range f.TelegrafConfigs {
if err := svc.DeleteTelegrafConfig(ctx, tc.ID); err != nil {
t.Logf("failed to remove telegraf config: %v", err)
}
}
}
}

45
kv/unique.go Normal file
View File

@ -0,0 +1,45 @@
package kv
import (
"context"
"fmt"
influxdb "github.com/influxdata/influxdb"
)
// UnexpectedIndexError is used when the error comes from an internal system.
func UnexpectedIndexError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving index; Err: %v", err),
Op: "kv/index",
}
}
// NotUniqueError is used when attempting to create a resource that already
// exists.
var NotUniqueError = &influxdb.Error{
Code: influxdb.EConflict,
Msg: fmt.Sprintf("name already exists"),
}
func (s *Service) unique(ctx context.Context, tx Tx, indexBucket, indexKey []byte) error {
bucket, err := tx.Bucket(indexBucket)
if err != nil {
return UnexpectedIndexError(err)
}
_, err = bucket.Get(indexKey)
// if not found then this is _unique_.
if IsNotFound(err) {
return nil
}
// no error means this is not unique
if err == nil {
return NotUniqueError
}
// any other error is some sort of internal server error
return UnexpectedIndexError(err)
}

360
kv/urm.go Normal file
View File

@ -0,0 +1,360 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"github.com/influxdata/influxdb"
)
var (
urmBucket = []byte("userresourcemappingsv1")
// ErrInvalidURMID is used when the service was provided
// an invalid ID format.
ErrInvalidURMID = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "provided user resource mapping ID has invalid format",
}
// ErrURMNotFound is used when the user resource mapping is not found.
ErrURMNotFound = &influxdb.Error{
Msg: "user to resource mapping not found",
Code: influxdb.ENotFound,
}
)
// UnavailableURMServiceError is used if we aren't able to interact with the
// store, it means the store is not available at the moment (e.g. network).
func UnavailableURMServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unable to connect to resource mapping service. Please try again; Err: %v", err),
Op: "kv/userResourceMapping",
}
}
// CorruptURMError is used when the config cannot be unmarshalled from the
// bytes stored in the kv.
func CorruptURMError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unknown internal user resource mapping data error; Err: %v", err),
Op: "kv/userResourceMapping",
}
}
// ErrUnprocessableMapping is used when a user resource mapping is not able to be converted to JSON.
func ErrUnprocessableMapping(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: fmt.Sprintf("unable to convert mapping of user to resource into JSON; Err %v", err),
}
}
// NonUniqueMappingError is an internal error when a user already has
// been mapped to a resource
func NonUniqueMappingError(userID influxdb.ID) error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("Unexpected error when assigning user to a resource: mapping for user %s already exists", userID.String()),
}
}
func (s *Service) initializeURMs(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket(urmBucket); err != nil {
return UnavailableURMServiceError(err)
}
return nil
}
func filterMappingsFn(filter influxdb.UserResourceMappingFilter) func(m *influxdb.UserResourceMapping) bool {
return func(mapping *influxdb.UserResourceMapping) bool {
return (!filter.UserID.Valid() || (filter.UserID == mapping.UserID)) &&
(!filter.ResourceID.Valid() || (filter.ResourceID == mapping.ResourceID)) &&
(filter.UserType == "" || (filter.UserType == mapping.UserType)) &&
(filter.ResourceType == "" || (filter.ResourceType == mapping.ResourceType))
}
}
// FindUserResourceMappings returns a list of UserResourceMappings that match filter and the total count of matching mappings.
func (s *Service) FindUserResourceMappings(ctx context.Context, filter influxdb.UserResourceMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.UserResourceMapping, int, error) {
var ms []*influxdb.UserResourceMapping
err := s.kv.View(func(tx Tx) error {
var err error
ms, err = s.findUserResourceMappings(ctx, tx, filter)
return err
})
if err != nil {
return nil, 0, err
}
return ms, len(ms), nil
}
func (s *Service) findUserResourceMappings(ctx context.Context, tx Tx, filter influxdb.UserResourceMappingFilter) ([]*influxdb.UserResourceMapping, error) {
ms := []*influxdb.UserResourceMapping{}
filterFn := filterMappingsFn(filter)
err := s.forEachUserResourceMapping(ctx, tx, func(m *influxdb.UserResourceMapping) bool {
if filterFn(m) {
ms = append(ms, m)
}
return true
})
return ms, err
}
func (s *Service) findUserResourceMapping(ctx context.Context, tx Tx, filter influxdb.UserResourceMappingFilter) (*influxdb.UserResourceMapping, error) {
ms, err := s.findUserResourceMappings(ctx, tx, filter)
if err != nil {
return nil, err
}
if len(ms) == 0 {
return nil, ErrURMNotFound
}
return ms[0], nil
}
// CreateUserResourceMapping associates a user to a resource either as a member
// or owner.
func (s *Service) CreateUserResourceMapping(ctx context.Context, m *influxdb.UserResourceMapping) error {
return s.kv.Update(func(tx Tx) error {
return s.createUserResourceMapping(ctx, tx, m)
})
}
func (s *Service) createUserResourceMapping(ctx context.Context, tx Tx, m *influxdb.UserResourceMapping) error {
if err := s.uniqueUserResourceMapping(ctx, tx, m); err != nil {
return err
}
v, err := json.Marshal(m)
if err != nil {
return ErrUnprocessableMapping(err)
}
key, err := userResourceKey(m)
if err != nil {
return err
}
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
if err := b.Put(key, v); err != nil {
return UnavailableURMServiceError(err)
}
if m.ResourceType == influxdb.OrgsResourceType {
return s.createOrgDependentMappings(ctx, tx, m)
}
return nil
}
// This method creates the user/resource mappings for resources that belong to an organization.
func (s *Service) createOrgDependentMappings(ctx context.Context, tx Tx, m *influxdb.UserResourceMapping) error {
bf := influxdb.BucketFilter{OrganizationID: &m.ResourceID}
bs, err := s.findBuckets(ctx, tx, bf)
if err != nil {
return err
}
for _, b := range bs {
m := &influxdb.UserResourceMapping{
ResourceType: influxdb.BucketsResourceType,
ResourceID: b.ID,
UserType: m.UserType,
UserID: m.UserID,
}
if err := s.createUserResourceMapping(ctx, tx, m); err != nil {
return err
}
// TODO(desa): add support for all other resource types.
}
return nil
}
func userResourceKey(m *influxdb.UserResourceMapping) ([]byte, error) {
encodedResourceID, err := m.ResourceID.Encode()
if err != nil {
return nil, ErrInvalidURMID
}
encodedUserID, err := m.UserID.Encode()
if err != nil {
return nil, ErrInvalidURMID
}
key := make([]byte, len(encodedResourceID)+len(encodedUserID))
copy(key, encodedResourceID)
copy(key[len(encodedResourceID):], encodedUserID)
return key, nil
}
func (s *Service) forEachUserResourceMapping(ctx context.Context, tx Tx, fn func(*influxdb.UserResourceMapping) bool) error {
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
cur, err := b.Cursor()
if err != nil {
return UnavailableURMServiceError(err)
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
m := &influxdb.UserResourceMapping{}
if err := json.Unmarshal(v, m); err != nil {
return CorruptURMError(err)
}
if !fn(m) {
break
}
}
return nil
}
func (s *Service) uniqueUserResourceMapping(ctx context.Context, tx Tx, m *influxdb.UserResourceMapping) error {
key, err := userResourceKey(m)
if err != nil {
return err
}
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
_, err = b.Get(key)
if !IsNotFound(err) {
return NonUniqueMappingError(m.UserID)
}
return nil
}
// DeleteUserResourceMapping deletes a user resource mapping.
func (s *Service) DeleteUserResourceMapping(ctx context.Context, resourceID influxdb.ID, userID influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
// TODO(goller): I don't think this find is needed as delete also finds.
m, err := s.findUserResourceMapping(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceID: resourceID,
UserID: userID,
})
if err != nil {
return err
}
filter := influxdb.UserResourceMappingFilter{
ResourceID: resourceID,
UserID: userID,
}
if err := s.deleteUserResourceMapping(ctx, tx, filter); err != nil {
return err
}
if m.ResourceType == influxdb.OrgsResourceType {
return s.deleteOrgDependentMappings(ctx, tx, m)
}
return nil
})
}
func (s *Service) deleteUserResourceMapping(ctx context.Context, tx Tx, filter influxdb.UserResourceMappingFilter) error {
// TODO(goller): do we really need to find here? Seems like a Get is
// good enough.
ms, err := s.findUserResourceMappings(ctx, tx, filter)
if err != nil {
return err
}
if len(ms) == 0 {
return ErrURMNotFound
}
key, err := userResourceKey(ms[0])
if err != nil {
return err
}
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
_, err = b.Get(key)
if IsNotFound(err) {
return ErrURMNotFound
}
if err != nil {
return UnavailableURMServiceError(err)
}
if err := b.Delete(key); err != nil {
return UnavailableURMServiceError(err)
}
return nil
}
func (s *Service) deleteUserResourceMappings(ctx context.Context, tx Tx, filter influxdb.UserResourceMappingFilter) error {
ms, err := s.findUserResourceMappings(ctx, tx, filter)
if err != nil {
return err
}
for _, m := range ms {
key, err := userResourceKey(m)
if err != nil {
return err
}
b, err := tx.Bucket(urmBucket)
if err != nil {
return UnavailableURMServiceError(err)
}
_, err = b.Get(key)
if IsNotFound(err) {
return ErrURMNotFound
}
if err != nil {
return UnavailableURMServiceError(err)
}
if err := b.Delete(key); err != nil {
return UnavailableURMServiceError(err)
}
}
return nil
}
// This method deletes the user/resource mappings for resources that belong to an organization.
func (s *Service) deleteOrgDependentMappings(ctx context.Context, tx Tx, m *influxdb.UserResourceMapping) error {
bf := influxdb.BucketFilter{OrganizationID: &m.ResourceID}
bs, err := s.findBuckets(ctx, tx, bf)
if err != nil {
return err
}
for _, b := range bs {
if err := s.deleteUserResourceMapping(ctx, tx, influxdb.UserResourceMappingFilter{
ResourceType: influxdb.BucketsResourceType,
ResourceID: b.ID,
UserID: m.UserID,
}); err != nil {
return err
}
// TODO(desa): add support for all other resource types.
}
return nil
}

67
kv/urm_test.go Normal file
View File

@ -0,0 +1,67 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltUserResourceMappingService(t *testing.T) {
influxdbtesting.UserResourceMappingService(initBoltUserResourceMappingService, t)
}
func TestInmemUserResourceMappingService(t *testing.T) {
influxdbtesting.UserResourceMappingService(initInmemUserResourceMappingService, t)
}
func initBoltUserResourceMappingService(f influxdbtesting.UserResourceFields, t *testing.T) (influxdb.UserResourceMappingService, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initUserResourceMappingService(s, f, t)
return svc, func() {
closeSvc()
closeBolt()
}
}
func initInmemUserResourceMappingService(f influxdbtesting.UserResourceFields, t *testing.T) (influxdb.UserResourceMappingService, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, closeSvc := initUserResourceMappingService(s, f, t)
return svc, func() {
closeSvc()
closeBolt()
}
}
func initUserResourceMappingService(s kv.Store, f influxdbtesting.UserResourceFields, t *testing.T) (influxdb.UserResourceMappingService, func()) {
svc := kv.NewService(s)
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing urm service: %v", err)
}
for _, m := range f.UserResourceMappings {
if err := svc.CreateUserResourceMapping(ctx, m); err != nil {
t.Fatalf("failed to populate mappings")
}
}
return svc, func() {
for _, m := range f.UserResourceMappings {
if err := svc.DeleteUserResourceMapping(ctx, m.ResourceID, m.UserID); err != nil {
t.Logf("failed to remove user resource mapping: %v", err)
}
}
}
}

602
kv/user.go Normal file
View File

@ -0,0 +1,602 @@
package kv
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/influxdata/influxdb"
icontext "github.com/influxdata/influxdb/context"
)
var (
userBucket = []byte("usersv1")
userIndex = []byte("userindexv1")
)
var _ influxdb.UserService = (*Service)(nil)
var _ influxdb.UserOperationLogService = (*Service)(nil)
// Initialize creates the buckets for the user service.
func (s *Service) initializeUsers(ctx context.Context, tx Tx) error {
if _, err := s.userBucket(tx); err != nil {
return err
}
if _, err := s.userIndexBucket(tx); err != nil {
return err
}
return nil
}
func (s *Service) userBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket([]byte(userBucket))
if err != nil {
return nil, UnexpectedUserBucketError(err)
}
return b, nil
}
func (s *Service) userIndexBucket(tx Tx) (Bucket, error) {
b, err := tx.Bucket([]byte(userIndex))
if err != nil {
return nil, UnexpectedUserIndexError(err)
}
return b, nil
}
// FindUserByID retrieves a user by id.
func (s *Service) FindUserByID(ctx context.Context, id influxdb.ID) (*influxdb.User, error) {
var u *influxdb.User
err := s.kv.View(func(tx Tx) error {
usr, err := s.findUserByID(ctx, tx, id)
if err != nil {
return err
}
u = usr
return nil
})
if err != nil {
return nil, err
}
return u, nil
}
func (s *Service) findUserByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.User, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, InvalidUserIDError(err)
}
b, err := s.userBucket(tx)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if IsNotFound(err) {
return nil, ErrUserNotFound
}
if err != nil {
return nil, ErrInternalUserServiceError(err)
}
return UnmarshalUser(v)
}
// UnmarshalUser turns the stored byte slice in the kv into a *influxdb.User.
func UnmarshalUser(v []byte) (*influxdb.User, error) {
u := &influxdb.User{}
if err := json.Unmarshal(v, u); err != nil {
return nil, ErrCorruptUser(err)
}
return u, nil
}
// MarshalUser turns an *influxdb.User into a byte slice.
func MarshalUser(u *influxdb.User) ([]byte, error) {
v, err := json.Marshal(u)
if err != nil {
return nil, ErrUnprocessableUser(err)
}
return v, nil
}
// FindUserByName returns a user by name for a particular user.
func (s *Service) FindUserByName(ctx context.Context, n string) (*influxdb.User, error) {
var u *influxdb.User
err := s.kv.View(func(tx Tx) error {
usr, err := s.findUserByName(ctx, tx, n)
if err != nil {
return err
}
u = usr
return nil
})
return u, err
}
func (s *Service) findUserByName(ctx context.Context, tx Tx, n string) (*influxdb.User, error) {
b, err := s.userIndexBucket(tx)
if err != nil {
return nil, err
}
uid, err := b.Get(userIndexKey(n))
if err == ErrKeyNotFound {
return nil, ErrUserNotFound
}
if err != nil {
return nil, ErrInternalUserServiceError(err)
}
var id influxdb.ID
if err := id.Decode(uid); err != nil {
return nil, ErrCorruptUserID(err)
}
return s.findUserByID(ctx, tx, id)
}
// FindUser retrives a user using an arbitrary user filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across users until it finds a match.
func (s *Service) FindUser(ctx context.Context, filter influxdb.UserFilter) (*influxdb.User, error) {
if filter.ID != nil {
u, err := s.FindUserByID(ctx, *filter.ID)
if err != nil {
return nil, err
}
return u, nil
}
if filter.Name != nil {
return s.FindUserByName(ctx, *filter.Name)
}
return nil, ErrUserNotFound
}
func filterUsersFn(filter influxdb.UserFilter) func(u *influxdb.User) bool {
if filter.ID != nil {
return func(u *influxdb.User) bool {
return u.ID.Valid() && u.ID == *filter.ID
}
}
if filter.Name != nil {
return func(u *influxdb.User) bool {
return u.Name == *filter.Name
}
}
return func(u *influxdb.User) bool { return true }
}
// FindUsers retrives all users that match an arbitrary user filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across all users searching for a match.
func (s *Service) FindUsers(ctx context.Context, filter influxdb.UserFilter, opt ...influxdb.FindOptions) ([]*influxdb.User, int, error) {
if filter.ID != nil {
u, err := s.FindUserByID(ctx, *filter.ID)
if err != nil {
return nil, 0, err
}
return []*influxdb.User{u}, 1, nil
}
if filter.Name != nil {
u, err := s.FindUserByName(ctx, *filter.Name)
if err != nil {
return nil, 0, err
}
return []*influxdb.User{u}, 1, nil
}
us := []*influxdb.User{}
filterFn := filterUsersFn(filter)
err := s.kv.View(func(tx Tx) error {
return s.forEachUser(ctx, tx, func(u *influxdb.User) bool {
if filterFn(u) {
us = append(us, u)
}
return true
})
})
if err != nil {
return nil, 0, err
}
return us, len(us), nil
}
// CreateUser creates a influxdb user and sets b.ID.
func (s *Service) CreateUser(ctx context.Context, u *influxdb.User) error {
return s.kv.Update(func(tx Tx) error {
return s.createUser(ctx, tx, u)
})
}
func (s *Service) createUser(ctx context.Context, tx Tx, u *influxdb.User) error {
if err := s.uniqueUserName(ctx, tx, u); err != nil {
return err
}
u.ID = s.IDGenerator.ID()
if err := s.appendUserEventToLog(ctx, tx, u.ID, userCreatedEvent); err != nil {
return err
}
return s.putUser(ctx, tx, u)
}
// PutUser will put a user without setting an ID.
func (s *Service) PutUser(ctx context.Context, u *influxdb.User) error {
return s.kv.Update(func(tx Tx) error {
return s.putUser(ctx, tx, u)
})
}
func (s *Service) putUser(ctx context.Context, tx Tx, u *influxdb.User) error {
v, err := MarshalUser(u)
if err != nil {
return err
}
encodedID, err := u.ID.Encode()
if err != nil {
return InvalidUserIDError(err)
}
idx, err := s.userIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Put(userIndexKey(u.Name), encodedID); err != nil {
return ErrInternalUserServiceError(err)
}
b, err := s.userBucket(tx)
if err != nil {
return err
}
if err := b.Put(encodedID, v); err != nil {
return ErrInternalUserServiceError(err)
}
return nil
}
func userIndexKey(n string) []byte {
return []byte(n)
}
// forEachUser will iterate through all users while fn returns true.
func (s *Service) forEachUser(ctx context.Context, tx Tx, fn func(*influxdb.User) bool) error {
b, err := s.userBucket(tx)
if err != nil {
return err
}
cur, err := b.Cursor()
if err != nil {
return ErrInternalUserServiceError(err)
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
u, err := UnmarshalUser(v)
if err != nil {
return err
}
if !fn(u) {
break
}
}
return nil
}
func (s *Service) uniqueUserName(ctx context.Context, tx Tx, u *influxdb.User) error {
key := userIndexKey(u.Name)
// if the name is not unique across all users in all organizations, then,
// do not allow creation.
err := s.unique(ctx, tx, userIndex, key)
if err == NotUniqueError {
return UserAlreadyExistsError(u.Name)
}
return err
}
// UpdateUser updates a user according the parameters set on upd.
func (s *Service) UpdateUser(ctx context.Context, id influxdb.ID, upd influxdb.UserUpdate) (*influxdb.User, error) {
var u *influxdb.User
err := s.kv.Update(func(tx Tx) error {
usr, err := s.updateUser(ctx, tx, id, upd)
if err != nil {
return err
}
u = usr
return nil
})
if err != nil {
return nil, err
}
return u, nil
}
func (s *Service) updateUser(ctx context.Context, tx Tx, id influxdb.ID, upd influxdb.UserUpdate) (*influxdb.User, error) {
u, err := s.findUserByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.Name != nil {
if err := s.removeUserFromIndex(ctx, tx, id, *upd.Name); err != nil {
return nil, err
}
u.Name = *upd.Name
}
if err := s.appendUserEventToLog(ctx, tx, u.ID, userUpdatedEvent); err != nil {
return nil, err
}
if err := s.putUser(ctx, tx, u); err != nil {
return nil, err
}
return u, nil
}
func (s *Service) removeUserFromIndex(ctx context.Context, tx Tx, id influxdb.ID, name string) error {
// Users are indexed by name and so the user index must be pruned
// when name is modified.
idx, err := s.userIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Delete(userIndexKey(name)); err != nil {
return ErrInternalUserServiceError(err)
}
return nil
}
// DeleteUser deletes a user and prunes it from the index.
func (s *Service) DeleteUser(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
return s.deleteUser(ctx, tx, id)
})
}
func (s *Service) deleteUser(ctx context.Context, tx Tx, id influxdb.ID) error {
u, err := s.findUserByID(ctx, tx, id)
if err != nil {
return err
}
if err := s.deleteUsersAuthorizations(ctx, tx, id); err != nil {
return err
}
encodedID, err := id.Encode()
if err != nil {
return InvalidUserIDError(err)
}
idx, err := s.userIndexBucket(tx)
if err != nil {
return err
}
if err := idx.Delete(userIndexKey(u.Name)); err != nil {
return ErrInternalUserServiceError(err)
}
b, err := s.userBucket(tx)
if err != nil {
return err
}
if err := b.Delete(encodedID); err != nil {
return ErrInternalUserServiceError(err)
}
if err := s.deleteUserResourceMappings(ctx, tx, influxdb.UserResourceMappingFilter{
UserID: id,
}); err != nil {
return err
}
return nil
}
func (s *Service) deleteUsersAuthorizations(ctx context.Context, tx Tx, id influxdb.ID) error {
authFilter := influxdb.AuthorizationFilter{
UserID: &id,
}
as, err := s.findAuthorizations(ctx, tx, authFilter)
if err != nil {
return err
}
for _, a := range as {
if err := s.deleteAuthorization(ctx, tx, a.ID); err != nil {
return err
}
}
return nil
}
// GetUserOperationLog retrieves a user operation log.
func (s *Service) GetUserOperationLog(ctx context.Context, id influxdb.ID, opts influxdb.FindOptions) ([]*influxdb.OperationLogEntry, int, error) {
// TODO(desa): might be worthwhile to allocate a slice of size opts.Limit
log := []*influxdb.OperationLogEntry{}
err := s.kv.View(func(tx Tx) error {
key, err := encodeBucketOperationLogKey(id)
if err != nil {
return err
}
return s.forEachLogEntry(ctx, tx, key, opts, func(v []byte, t time.Time) error {
e := &influxdb.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
}
const userOperationLogKeyPrefix = "user"
// TODO(desa): what do we want these to be?
const (
userCreatedEvent = "User Created"
userUpdatedEvent = "User Updated"
)
func encodeUserOperationLogKey(id influxdb.ID) ([]byte, error) {
buf, err := id.Encode()
if err != nil {
return nil, err
}
return append([]byte(userOperationLogKeyPrefix), buf...), nil
}
func (s *Service) appendUserEventToLog(ctx context.Context, tx Tx, id influxdb.ID, st string) error {
e := &influxdb.OperationLogEntry{
Description: st,
}
// 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 := icontext.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 := encodeUserOperationLogKey(id)
if err != nil {
return err
}
return s.addLogEntry(ctx, tx, k, v, s.time())
}
var (
// ErrUserNotFound is used when the user is not found.
ErrUserNotFound = &influxdb.Error{
Msg: "user not found",
Code: influxdb.ENotFound,
}
)
// ErrInternalUserServiceError is used when the error comes from an internal system.
func ErrInternalUserServiceError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Err: err,
}
}
// UserAlreadyExistsError is used when attempting to create a user with a name
// that already exists.
func UserAlreadyExistsError(n string) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EConflict,
Msg: fmt.Sprintf("user with name %s already exists", n),
}
}
// UnexpectedUserBucketError is used when the error comes from an internal system.
func UnexpectedUserBucketError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving user bucket; Err: %v", err),
Op: "kv/userBucket",
}
}
// UnexpectedUserIndexError is used when the error comes from an internal system.
func UnexpectedUserIndexError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: fmt.Sprintf("unexpected error retrieving user index; Err: %v", err),
Op: "kv/userIndex",
}
}
// InvalidUserIDError is used when a service was provided an invalid ID.
// This is some sort of internal server error.
func InvalidUserIDError(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "user id provided is invalid",
Err: err,
}
}
// ErrCorruptUserID the ID stored in the Store is corrupt.
func ErrCorruptUserID(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "corrupt ID provided",
Err: err,
}
}
// ErrCorruptUser is used when the user cannot be unmarshalled from the bytes
// stored in the kv.
func ErrCorruptUser(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Msg: "user could not be unmarshalled",
Err: err,
Op: "kv/UnmarshalUser",
}
}
// ErrUnprocessableUser is used when a user is not able to be processed.
func ErrUnprocessableUser(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EUnprocessableEntity,
Msg: "user could not be marshalled",
Err: err,
Op: "kv/MarshalUser",
}
}

68
kv/user_test.go Normal file
View File

@ -0,0 +1,68 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltUserService(t *testing.T) {
influxdbtesting.UserService(initBoltUserService, t)
}
func TestInmemUserService(t *testing.T) {
influxdbtesting.UserService(initInmemUserService, t)
}
func initBoltUserService(f influxdbtesting.UserFields, t *testing.T) (influxdb.UserService, string, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initUserService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initInmemUserService(f influxdbtesting.UserFields, t *testing.T) (influxdb.UserService, string, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initUserService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initUserService(s kv.Store, f influxdbtesting.UserFields, t *testing.T) (influxdb.UserService, string, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing user service: %v", err)
}
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
return svc, kv.OpPrefix, func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove users: %v", err)
}
}
}
}

425
kv/variable.go Normal file
View File

@ -0,0 +1,425 @@
package kv
import (
"bytes"
"context"
"encoding/json"
influxdb "github.com/influxdata/influxdb"
)
var (
variableBucket = []byte("variablesv1")
variableOrgsIndex = []byte("variableorgsv1")
)
func (s *Service) initializeVariables(ctx context.Context, tx Tx) error {
if _, err := tx.Bucket(variableBucket); err != nil {
return err
}
if _, err := tx.Bucket(variableOrgsIndex); err != nil {
return err
}
return nil
}
func decodeVariableOrgsIndexKey(indexKey []byte) (orgID influxdb.ID, variableID influxdb.ID, err error) {
if len(indexKey) != 2*influxdb.IDLength {
return 0, 0, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "malformed variable orgs index key (please report this error)",
}
}
if err := (&orgID).Decode(indexKey[:influxdb.IDLength]); err != nil {
return 0, 0, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "bad org id",
Err: influxdb.ErrInvalidID,
}
}
if err := (&variableID).Decode(indexKey[influxdb.IDLength:]); err != nil {
return 0, 0, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "bad variable id",
Err: influxdb.ErrInvalidID,
}
}
return orgID, variableID, nil
}
func (s *Service) findOrganizationVariables(ctx context.Context, tx Tx, orgID influxdb.ID) ([]*influxdb.Variable, error) {
idx, err := tx.Bucket(variableOrgsIndex)
if err != nil {
return nil, err
}
// TODO(leodido): support find options
cur, err := idx.Cursor()
if err != nil {
return nil, err
}
prefix, err := orgID.Encode()
if err != nil {
return nil, err
}
variables := []*influxdb.Variable{}
for k, _ := cur.Seek(prefix); bytes.HasPrefix(k, prefix); k, _ = cur.Next() {
_, id, err := decodeVariableOrgsIndexKey(k)
if err != nil {
return nil, err
}
m, err := s.findVariableByID(ctx, tx, id)
if err != nil {
return nil, err
}
variables = append(variables, m)
}
return variables, nil
}
func (s *Service) findVariables(ctx context.Context, tx Tx, filter influxdb.VariableFilter) ([]*influxdb.Variable, error) {
if filter.OrganizationID != nil {
return s.findOrganizationVariables(ctx, tx, *filter.OrganizationID)
}
if filter.Organization != nil {
o, err := s.findOrganizationByName(ctx, tx, *filter.Organization)
if err != nil {
return nil, err
}
return s.findOrganizationVariables(ctx, tx, o.ID)
}
variables := []*influxdb.Variable{}
filterFn := filterVariablesFn(filter)
err := s.forEachVariable(ctx, tx, func(m *influxdb.Variable) bool {
if filterFn(m) {
variables = append(variables, m)
}
return true
})
if err != nil {
return nil, err
}
return variables, nil
}
func filterVariablesFn(filter influxdb.VariableFilter) func(m *influxdb.Variable) bool {
if filter.ID != nil {
return func(m *influxdb.Variable) bool {
return m.ID == *filter.ID
}
}
if filter.OrganizationID != nil {
return func(m *influxdb.Variable) bool {
return m.OrganizationID == *filter.OrganizationID
}
}
return func(m *influxdb.Variable) bool { return true }
}
// forEachVariable will iterate through all variables while fn returns true.
func (s *Service) forEachVariable(ctx context.Context, tx Tx, fn func(*influxdb.Variable) bool) error {
b, err := tx.Bucket(variableBucket)
if err != nil {
return err
}
cur, err := b.Cursor()
if err != nil {
return err
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
m := &influxdb.Variable{}
if err := json.Unmarshal(v, m); err != nil {
return err
}
if !fn(m) {
break
}
}
return nil
}
// FindVariables returns all variables in the store
func (s *Service) FindVariables(ctx context.Context, filter influxdb.VariableFilter, opt ...influxdb.FindOptions) ([]*influxdb.Variable, error) {
// todo(leodido) > handle find options
res := []*influxdb.Variable{}
err := s.kv.View(func(tx Tx) error {
variables, err := s.findVariables(ctx, tx, filter)
if err != nil && influxdb.ErrorCode(err) != influxdb.ENotFound {
return err
}
res = variables
return nil
})
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return res, nil
}
// FindVariableByID finds a single variable in the store by its ID
func (s *Service) FindVariableByID(ctx context.Context, id influxdb.ID) (*influxdb.Variable, error) {
var variable *influxdb.Variable
err := s.kv.View(func(tx Tx) error {
m, pe := s.findVariableByID(ctx, tx, id)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
variable = m
return nil
})
if err != nil {
return nil, err
}
return variable, nil
}
func (s *Service) findVariableByID(ctx context.Context, tx Tx, id influxdb.ID) (*influxdb.Variable, error) {
encID, err := id.Encode()
if err != nil {
return nil, &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
b, err := tx.Bucket(variableBucket)
if err != nil {
return nil, err
}
d, err := b.Get(encID)
if IsNotFound(err) {
return nil, &influxdb.Error{
Code: influxdb.ENotFound,
Msg: influxdb.ErrVariableNotFound,
}
}
if err != nil {
return nil, err
}
variable := &influxdb.Variable{}
err = json.Unmarshal(d, &variable)
if err != nil {
return nil, &influxdb.Error{
Err: err,
}
}
return variable, nil
}
// CreateVariable creates a new variable and assigns it an ID
func (s *Service) CreateVariable(ctx context.Context, variable *influxdb.Variable) error {
return s.kv.Update(func(tx Tx) error {
variable.ID = s.IDGenerator.ID()
if err := s.putVariableOrgsIndex(ctx, tx, variable); err != nil {
return err
}
if pe := s.putVariable(ctx, tx, variable); pe != nil {
return &influxdb.Error{
Err: pe,
}
}
return nil
})
}
// ReplaceVariable puts a variable in the store
func (s *Service) ReplaceVariable(ctx context.Context, variable *influxdb.Variable) error {
return s.kv.Update(func(tx Tx) error {
if err := s.putVariableOrgsIndex(ctx, tx, variable); err != nil {
return &influxdb.Error{
Err: err,
}
}
return s.putVariable(ctx, tx, variable)
})
}
func encodeVariableOrgsIndex(variable *influxdb.Variable) ([]byte, error) {
oID, err := variable.OrganizationID.Encode()
if err != nil {
return nil, &influxdb.Error{
Err: err,
Msg: "bad organization id",
}
}
mID, err := variable.ID.Encode()
if err != nil {
return nil, &influxdb.Error{
Err: err,
Msg: "bad variable id",
}
}
key := make([]byte, 0, influxdb.IDLength*2)
key = append(key, oID...)
key = append(key, mID...)
return key, nil
}
func (s *Service) putVariableOrgsIndex(ctx context.Context, tx Tx, variable *influxdb.Variable) error {
key, err := encodeVariableOrgsIndex(variable)
if err != nil {
return err
}
idx, err := tx.Bucket(variableOrgsIndex)
if err != nil {
return err
}
if err := idx.Put(key, nil); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
func (s *Service) removeVariableOrgsIndex(ctx context.Context, tx Tx, variable *influxdb.Variable) error {
key, err := encodeVariableOrgsIndex(variable)
if err != nil {
return err
}
idx, err := tx.Bucket(variableOrgsIndex)
if err != nil {
return err
}
if err := idx.Delete(key); err != nil {
return err
}
return nil
}
func (s *Service) putVariable(ctx context.Context, tx Tx, variable *influxdb.Variable) error {
m, err := json.Marshal(variable)
if err != nil {
return &influxdb.Error{
Err: err,
}
}
encID, err := variable.ID.Encode()
if err != nil {
return &influxdb.Error{
Code: influxdb.EInvalid,
Err: err,
}
}
b, err := tx.Bucket(variableBucket)
if err != nil {
return err
}
if err := b.Put(encID, m); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
}
// UpdateVariable updates a single variable in the store with a changeset
func (s *Service) UpdateVariable(ctx context.Context, id influxdb.ID, update *influxdb.VariableUpdate) (*influxdb.Variable, error) {
var variable *influxdb.Variable
err := s.kv.Update(func(tx Tx) error {
m, pe := s.findVariableByID(ctx, tx, id)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
if err := update.Apply(m); err != nil {
return &influxdb.Error{
Err: err,
}
}
variable = m
if pe = s.putVariable(ctx, tx, variable); pe != nil {
return &influxdb.Error{
Err: pe,
}
}
return nil
})
return variable, err
}
// DeleteVariable removes a single variable from the store by its ID
func (s *Service) DeleteVariable(ctx context.Context, id influxdb.ID) error {
return s.kv.Update(func(tx Tx) error {
m, pe := s.findVariableByID(ctx, tx, id)
if pe != nil {
return &influxdb.Error{
Err: pe,
}
}
encID, err := id.Encode()
if err != nil {
return &influxdb.Error{
Err: err,
}
}
if err := s.removeVariableOrgsIndex(ctx, tx, m); err != nil {
return &influxdb.Error{
Err: err,
}
}
b, err := tx.Bucket(variableBucket)
if err != nil {
return err
}
if err := b.Delete(encID); err != nil {
return &influxdb.Error{
Err: err,
}
}
return nil
})
}

69
kv/variable_test.go Normal file
View File

@ -0,0 +1,69 @@
package kv_test
import (
"context"
"testing"
"github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kv"
influxdbtesting "github.com/influxdata/influxdb/testing"
)
func TestBoltVariableService(t *testing.T) {
influxdbtesting.VariableService(initBoltVariableService, t)
}
func TestInmemVariableService(t *testing.T) {
influxdbtesting.VariableService(initInmemVariableService, t)
}
func initBoltVariableService(f influxdbtesting.VariableFields, t *testing.T) (influxdb.VariableService, string, func()) {
s, closeBolt, err := NewTestBoltStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initVariableService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initInmemVariableService(f influxdbtesting.VariableFields, t *testing.T) (influxdb.VariableService, string, func()) {
s, closeBolt, err := NewTestInmemStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc, op, closeSvc := initVariableService(s, f, t)
return svc, op, func() {
closeSvc()
closeBolt()
}
}
func initVariableService(s kv.Store, f influxdbtesting.VariableFields, t *testing.T) (influxdb.VariableService, string, func()) {
svc := kv.NewService(s)
svc.IDGenerator = f.IDGenerator
ctx := context.Background()
if err := svc.Initialize(ctx); err != nil {
t.Fatalf("error initializing variable service: %v", err)
}
for _, variable := range f.Variables {
if err := svc.ReplaceVariable(ctx, variable); err != nil {
t.Fatalf("failed to populate test variables: %v", err)
}
}
done := func() {
for _, variable := range f.Variables {
if err := svc.DeleteVariable(ctx, variable.ID); err != nil {
t.Fatalf("failed to clean up variables bolt test: %v", err)
}
}
}
return svc, kv.OpPrefix, done
}

118
mock/kv.go Normal file
View File

@ -0,0 +1,118 @@
package mock
import (
"context"
"github.com/influxdata/influxdb/kv"
)
var _ (kv.Store) = (*Store)(nil)
// Store is a mock kv.Store
type Store struct {
ViewFn func(func(kv.Tx) error) error
UpdateFn func(func(kv.Tx) error) error
}
// View opens up a transaction that will not write to any data. Implementing interfaces
// should take care to ensure that all view transactions do not mutate any data.
func (s *Store) View(fn func(kv.Tx) error) error {
return s.ViewFn(fn)
}
// Update opens up a transaction that will mutate data.
func (s *Store) Update(fn func(kv.Tx) error) error {
return s.UpdateFn(fn)
}
var _ (kv.Tx) = (*Tx)(nil)
// Tx is mock of a kv.Tx.
type Tx struct {
BucketFn func(b []byte) (kv.Bucket, error)
ContextFn func() context.Context
WithContextFn func(ctx context.Context)
}
// Bucket possibly creates and returns bucket, b.
func (t *Tx) Bucket(b []byte) (kv.Bucket, error) {
return t.BucketFn(b)
}
// Context returns the context associated with this Tx.
func (t *Tx) Context() context.Context {
return t.ContextFn()
}
// WithContext associates a context with this Tx.
func (t *Tx) WithContext(ctx context.Context) {
t.WithContextFn(ctx)
}
var _ (kv.Bucket) = (*Bucket)(nil)
// Bucket is the abstraction used to perform get/put/delete/get-many operations
// in a key value store
type Bucket struct {
GetFn func(key []byte) ([]byte, error)
CursorFn func() (kv.Cursor, error)
PutFn func(key, value []byte) error
DeleteFn func(key []byte) error
}
// Get returns a key within this bucket. Errors if key does not exist.
func (b *Bucket) Get(key []byte) ([]byte, error) {
return b.GetFn(key)
}
// Cursor returns a cursor at the beginning of this bucket.
func (b *Bucket) Cursor() (kv.Cursor, error) {
return b.CursorFn()
}
// Put should error if the transaction it was called in is not writable.
func (b *Bucket) Put(key, value []byte) error {
return b.PutFn(key, value)
}
// Delete should error if the transaction it was called in is not writable.
func (b *Bucket) Delete(key []byte) error {
return b.DeleteFn(key)
}
var _ (kv.Cursor) = (*Cursor)(nil)
// Cursor is an abstraction for iterating/ranging through data. A concrete implementation
// of a cursor can be found in cursor.go.
type Cursor struct {
SeekFn func(prefix []byte) (k []byte, v []byte)
FirstFn func() (k []byte, v []byte)
LastFn func() (k []byte, v []byte)
NextFn func() (k []byte, v []byte)
PrevFn func() (k []byte, v []byte)
}
// Seek moves the cursor forward until reaching prefix in the key name.
func (c *Cursor) Seek(prefix []byte) (k []byte, v []byte) {
return c.SeekFn(prefix)
}
// First moves the cursor to the first key in the bucket.
func (c *Cursor) First() (k []byte, v []byte) {
return c.FirstFn()
}
// Last moves the cursor to the last key in the bucket.
func (c *Cursor) Last() (k []byte, v []byte) {
return c.LastFn()
}
// Next moves the cursor to the next key in the bucket.
func (c *Cursor) Next() (k []byte, v []byte) {
return c.NextFn()
}
// Prev moves the cursor to the prev key in the bucket.
func (c *Cursor) Prev() (k []byte, v []byte) {
return c.PrevFn()
}

View File

@ -10,7 +10,7 @@ var _ platform.OnboardingService = (*OnboardingService)(nil)
// OnboardingService is a mock implementation of platform.OnboardingService.
type OnboardingService struct {
BasicAuthService
PasswordsService
BucketService
OrganizationService
UserService

View File

@ -5,18 +5,18 @@ import (
"fmt"
)
// BasicAuthService is a mock implementation of a retention.BasicAuthService, which
// also makes it a suitable mock to use wherever an platform.BasicAuthService is required.
type BasicAuthService struct {
// PasswordsService is a mock implementation of a retention.PasswordsService, which
// also makes it a suitable mock to use wherever an platform.PasswordsService is required.
type PasswordsService struct {
SetPasswordFn func(context.Context, string, string) error
ComparePasswordFn func(context.Context, string, string) error
CompareAndSetPasswordFn func(context.Context, string, string, string) error
}
// NewBasicAuthService returns a mock BasicAuthService where its methods will return
// NewPasswordsService returns a mock PasswordsService where its methods will return
// zero values.
func NewBasicAuthService(user, password string) *BasicAuthService {
return &BasicAuthService{
func NewPasswordsService(user, password string) *PasswordsService {
return &PasswordsService{
SetPasswordFn: func(context.Context, string, string) error { return fmt.Errorf("mock error") },
ComparePasswordFn: func(context.Context, string, string) error { return fmt.Errorf("mock error") },
CompareAndSetPasswordFn: func(context.Context, string, string, string) error { return fmt.Errorf("mock error") },
@ -24,16 +24,16 @@ func NewBasicAuthService(user, password string) *BasicAuthService {
}
// SetPassword sets the users current password to be the provided password.
func (s *BasicAuthService) SetPassword(ctx context.Context, name string, password string) error {
func (s *PasswordsService) SetPassword(ctx context.Context, name string, password string) error {
return s.SetPasswordFn(ctx, name, password)
}
// ComparePassword password compares the provided password.
func (s *BasicAuthService) ComparePassword(ctx context.Context, name string, password string) error {
func (s *PasswordsService) ComparePassword(ctx context.Context, name string, password string) error {
return s.ComparePasswordFn(ctx, name, password)
}
// CompareAndSetPassword compares the provided password and sets it to the new password.
func (s *BasicAuthService) CompareAndSetPassword(ctx context.Context, name string, old string, new string) error {
func (s *PasswordsService) CompareAndSetPassword(ctx context.Context, name string, old string, new string) error {
return s.CompareAndSetPasswordFn(ctx, name, old, new)
}

View File

@ -41,11 +41,6 @@ func (s *TelegrafConfigStore) FindTelegrafConfigByID(ctx context.Context, id pla
return s.FindTelegrafConfigByIDF(ctx, id)
}
// FindTelegrafConfig returns the first telegraf config that matches filter.
func (s *TelegrafConfigStore) FindTelegrafConfig(ctx context.Context, filter platform.TelegrafConfigFilter) (*platform.TelegrafConfig, error) {
return s.FindTelegrafConfigF(ctx, filter)
}
// FindTelegrafConfigs returns a list of telegraf configs that match filter and the total count of matching telegraf configs.
// Additional options provide pagination & sorting.
func (s *TelegrafConfigStore) FindTelegrafConfigs(ctx context.Context, filter platform.TelegrafConfigFilter, opt ...platform.FindOptions) ([]*platform.TelegrafConfig, int, error) {

View File

@ -2,6 +2,20 @@ package influxdb
import "context"
// OnboardingService represents a service for the first run.
type OnboardingService interface {
PasswordsService
BucketService
OrganizationService
UserService
AuthorizationService
// IsOnboarding determine if onboarding request is allowed.
IsOnboarding(ctx context.Context) (bool, error)
// Generate OnboardingResults.
Generate(ctx context.Context, req *OnboardingRequest) (*OnboardingResults, error)
}
// OnboardingResults is a group of elements required for first run.
type OnboardingResults struct {
User *User `json:"user"`
@ -21,16 +35,33 @@ type OnboardingRequest struct {
Token string `json:"token,omitempty"`
}
// OnboardingService represents a service for the first run.
type OnboardingService interface {
BasicAuthService
BucketService
OrganizationService
UserService
AuthorizationService
func (r *OnboardingRequest) Valid() error {
if r.Password == "" {
return &Error{
Code: EEmptyValue,
Msg: "password is empty",
}
}
// IsOnboarding determine if onboarding request is allowed.
IsOnboarding(ctx context.Context) (bool, error)
// Generate OnboardingResults.
Generate(ctx context.Context, req *OnboardingRequest) (*OnboardingResults, error)
if r.User == "" {
return &Error{
Code: EEmptyValue,
Msg: "username is empty",
}
}
if r.Org == "" {
return &Error{
Code: EEmptyValue,
Msg: "org name is empty",
}
}
if r.Bucket == "" {
return &Error{
Code: EEmptyValue,
Msg: "bucket name is empty",
}
}
return nil
}

15
passwords.go Normal file
View File

@ -0,0 +1,15 @@
package influxdb
import "context"
// PasswordsService is the service for managing basic auth passwords.
type PasswordsService interface {
// SetPassword overrides the password of a known user.
SetPassword(ctx context.Context, name string, password string) error
// ComparePassword checks if the password matches the password recorded.
// Passwords that do not match return errors.
ComparePassword(ctx context.Context, name string, password string) error
// CompareAndSetPassword checks the password and if they match
// updates to the new password.
CompareAndSetPassword(ctx context.Context, name string, old string, new string) error
}

View File

@ -18,8 +18,8 @@ func TestOnboardingValidation(t *testing.T) {
validator := task.NewValidator(mockTaskService(), svc)
r, err := svc.Generate(context.Background(), &influxdb.OnboardingRequest{
User: "dude",
Password: "secret",
User: "Setec Astronomy",
Password: "too many secrets",
Org: "thing",
Bucket: "holder",
RetentionPeriod: 1,
@ -112,8 +112,8 @@ func TestValidations(t *testing.T) {
validTaskService := task.NewValidator(mockTaskService(), inmem)
r, err := inmem.Generate(context.Background(), &influxdb.OnboardingRequest{
User: "dude",
Password: "secret",
User: "Setec Astronomy",
Password: "too many secrets",
Org: "thing",
Bucket: "holder",
RetentionPeriod: 1,

View File

@ -16,12 +16,11 @@ import (
const ErrTelegrafConfigInvalidOrganizationID = "invalid organization ID"
// ErrTelegrafConfigNotFound is the error message for a missing telegraf config.
const ErrTelegrafConfigNotFound = "telegraf config not found"
const ErrTelegrafConfigNotFound = "telegraf configuration not found"
// ops for buckets error and buckets op logs.
var (
OpFindTelegrafConfigByID = "FindTelegrafConfigByID"
OpFindTelegrafConfig = "FindTelegrafConfig"
OpFindTelegrafConfigs = "FindTelegrafConfigs"
OpCreateTelegrafConfig = "CreateTelegrafConfig"
OpUpdateTelegrafConfig = "UpdateTelegrafConfig"
@ -37,9 +36,6 @@ type TelegrafConfigStore interface {
// FindTelegrafConfigByID returns a single telegraf config by ID.
FindTelegrafConfigByID(ctx context.Context, id ID) (*TelegrafConfig, error)
// FindTelegrafConfig returns the first telegraf config that matches filter.
FindTelegrafConfig(ctx context.Context, filter TelegrafConfigFilter) (*TelegrafConfig, error)
// FindTelegrafConfigs returns a list of telegraf configs that match filter and the total count of matching telegraf configs.
// Additional options provide pagination & sorting.
FindTelegrafConfigs(ctx context.Context, filter TelegrafConfigFilter, opt ...FindOptions) ([]*TelegrafConfig, int, error)

View File

@ -1,171 +0,0 @@
package testing
import (
"context"
"fmt"
"testing"
platform "github.com/influxdata/influxdb"
)
// BasicAuth test all the services for basic auth
func BasicAuth(
init func(UserFields, *testing.T) (platform.BasicAuthService, func()),
t *testing.T) {
type args struct {
name string
user string
setPassword string
comparePassword string
}
type wants struct {
setErr error
compareErr error
}
tests := []struct {
fields UserFields
args args
wants wants
}{
{
fields: UserFields{
Users: []*platform.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
},
args: args{
name: "happy path",
user: "user1",
setPassword: "hello",
comparePassword: "hello",
},
wants: wants{},
},
{
fields: UserFields{
Users: []*platform.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
},
args: args{
name: "happy path dont match",
user: "user1",
setPassword: "hello",
comparePassword: "world",
},
wants: wants{
compareErr: fmt.Errorf("crypto/bcrypt: hashedPassword is not the hash of the given password"),
},
},
}
for _, tt := range tests {
t.Run(tt.args.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.Background()
err := s.SetPassword(ctx, tt.args.user, tt.args.setPassword)
if (err != nil && tt.wants.setErr == nil) || (err == nil && tt.wants.setErr != nil) {
t.Fatalf("expected SetPassword error %v got %v", tt.wants.setErr, err)
return
}
if err != nil {
if want, got := tt.wants.setErr.Error(), err.Error(); want != got {
t.Fatalf("expected SetPassword error %v got %v", want, got)
}
return
}
err = s.ComparePassword(ctx, tt.args.user, tt.args.comparePassword)
if (err != nil && tt.wants.compareErr == nil) || (err == nil && tt.wants.compareErr != nil) {
t.Fatalf("expected ComparePassword error %v got %v", tt.wants.compareErr, err)
return
}
if err != nil {
if want, got := tt.wants.compareErr.Error(), err.Error(); want != got {
t.Fatalf("expected ComparePassword error %v got %v", tt.wants.compareErr, err)
}
return
}
})
}
}
// CompareAndSetPassword test
func CompareAndSetPassword(
init func(UserFields, *testing.T) (platform.BasicAuthService, func()),
t *testing.T) {
type args struct {
name string
user string
old string
new string
}
type wants struct {
err error
}
tests := []struct {
fields UserFields
args args
wants wants
}{
{
fields: UserFields{
Users: []*platform.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
},
args: args{
name: "happy path",
user: "user1",
old: "hello",
new: "hello",
},
wants: wants{},
},
}
for _, tt := range tests {
t.Run(tt.args.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.Background()
if err := s.SetPassword(ctx, tt.args.user, tt.args.old); err != nil {
t.Fatalf("unexpected error %v", err)
return
}
err := s.CompareAndSetPassword(ctx, tt.args.user, tt.args.old, tt.args.new)
if (err != nil && tt.wants.err == nil) || (err == nil && tt.wants.err != nil) {
t.Fatalf("expected CompareAndSetPassword error %v got %v", tt.wants.err, err)
return
}
if err != nil {
if want, got := tt.wants.err.Error(), err.Error(); want != got {
t.Fatalf("expected CompareAndSetPassword error %v got %v", tt.wants.err, err)
}
return
}
})
}
}

View File

@ -141,12 +141,12 @@ func Generate(
User: "admin",
Org: "org1",
Bucket: "bucket1",
Password: "pass1",
Password: "password1",
RetentionPeriod: 24 * 7, // 1 week
},
},
wants: wants{
password: "pass1",
password: "password1",
results: &platform.OnboardingResults{
User: &platform.User{
ID: MustIDBase16(oneID),
@ -183,10 +183,12 @@ func Generate(
ctx := context.Background()
results, err := s.Generate(ctx, tt.args.request)
if (err != nil) != (tt.wants.errCode != "") {
t.Logf("Error: %v", err)
t.Fatalf("expected error code '%s' got '%v'", tt.wants.errCode, err)
}
if err != nil && tt.wants.errCode != "" {
if code := platform.ErrorCode(err); code != tt.wants.errCode {
t.Logf("Error: %v", err)
t.Fatalf("expected error code to match '%s' got '%v'", tt.wants.errCode, code)
}
}

358
testing/passwords.go Normal file
View File

@ -0,0 +1,358 @@
package testing
import (
"context"
"fmt"
"testing"
"github.com/influxdata/influxdb"
)
// PasswordFields will include the IDGenerator, and users and their passwords.
type PasswordFields struct {
IDGenerator influxdb.IDGenerator
Users []*influxdb.User
Passwords []string // passwords are indexed against the Users field
}
// PasswordsService tests all the service functions.
func PasswordsService(
init func(PasswordFields, *testing.T) (influxdb.PasswordsService, func()), t *testing.T,
) {
tests := []struct {
name string
fn func(init func(PasswordFields, *testing.T) (influxdb.PasswordsService, func()),
t *testing.T)
}{
{
name: "SetPassword",
fn: SetPassword,
},
{
name: "ComparePassword",
fn: ComparePassword,
},
{
name: "CompareAndSetPassword",
fn: CompareAndSetPassword,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.fn(init, t)
})
}
}
// SetPassword tests overriding the password of a known user
func SetPassword(
init func(PasswordFields, *testing.T) (influxdb.PasswordsService, func()),
t *testing.T) {
type args struct {
user string
password string
}
type wants struct {
err error
}
tests := []struct {
name string
fields PasswordFields
args args
wants wants
}{
{
name: "setting password longer than 8 characters works",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
},
args: args{
user: "user1",
password: "howdydoody",
},
wants: wants{},
},
{
name: "passwords that are too short have errors",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
},
args: args{
user: "user1",
password: "short",
},
wants: wants{
err: fmt.Errorf("<invalid> passwords are required to be longer than 8 characters"),
},
},
{
name: "setting a password for a non-existent user is a generic-like error",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
},
args: args{
user: "invalid",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("<forbidden> your username or password is incorrect"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.Background()
err := s.SetPassword(ctx, tt.args.user, tt.args.password)
if (err != nil && tt.wants.err == nil) || (err == nil && tt.wants.err != nil) {
t.Fatalf("expected SetPassword error %v got %v", tt.wants.err, err)
return
}
if err != nil {
if want, got := tt.wants.err.Error(), err.Error(); want != got {
t.Fatalf("expected SetPassword error %v got %v", want, got)
}
return
}
})
}
}
// ComparePassword tests setting and comparing passwords.
func ComparePassword(
init func(PasswordFields, *testing.T) (influxdb.PasswordsService, func()),
t *testing.T) {
type args struct {
user string
password string
}
type wants struct {
err error
}
tests := []struct {
name string
fields PasswordFields
args args
wants wants
}{
{
name: "comparing same password is not an error",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
Passwords: []string{"howdydoody"},
},
args: args{
user: "user1",
password: "howdydoody",
},
wants: wants{},
},
{
name: "comparing different password is an error",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
Passwords: []string{"howdydoody"},
},
args: args{
user: "user1",
password: "wrongpassword",
},
wants: wants{
err: fmt.Errorf("<forbidden> your username or password is incorrect"),
},
},
{
name: "comparing a password to a non-existent user is a generic-like error",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
Passwords: []string{"howdydoody"},
},
args: args{
user: "invalid",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("<forbidden> your username or password is incorrect"),
},
},
{
name: "user exists but no password has been set",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
},
args: args{
user: "user1",
password: "howdydoody",
},
wants: wants{
err: fmt.Errorf("<forbidden> your username or password is incorrect"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.Background()
err := s.ComparePassword(ctx, tt.args.user, tt.args.password)
if (err != nil && tt.wants.err == nil) || (err == nil && tt.wants.err != nil) {
t.Fatalf("expected ComparePassword error %v got %v", tt.wants.err, err)
return
}
if err != nil {
if want, got := tt.wants.err.Error(), err.Error(); want != got {
t.Fatalf("expected ComparePassword error %v got %v", tt.wants.err, err)
}
return
}
})
}
}
// CompareAndSetPassword tests implementations of PasswordsService.
func CompareAndSetPassword(
init func(PasswordFields, *testing.T) (influxdb.PasswordsService, func()),
t *testing.T) {
type args struct {
user string
old string
new string
}
type wants struct {
err error
}
tests := []struct {
name string
fields PasswordFields
args args
wants wants
}{
{
name: "setting a password to the existing password is valid",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
Passwords: []string{"howdydoody"},
},
args: args{
user: "user1",
old: "howdydoody",
new: "howdydoody",
},
wants: wants{},
},
{
name: "providing an incorrect old password is an error",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
Passwords: []string{"howdydoody"},
},
args: args{
user: "user1",
old: "invalid",
new: "not used",
},
wants: wants{
err: fmt.Errorf("<forbidden> your username or password is incorrect"),
},
},
{
name: "<invalid> a new password that is less than 8 characters is an error",
fields: PasswordFields{
Users: []*influxdb.User{
{
Name: "user1",
ID: MustIDBase16(oneID),
},
},
Passwords: []string{"howdydoody"},
},
args: args{
user: "user1",
old: "howdydoody",
new: "short",
},
wants: wants{
err: fmt.Errorf("<invalid> passwords are required to be longer than 8 characters"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.Background()
err := s.CompareAndSetPassword(ctx, tt.args.user, tt.args.old, tt.args.new)
if (err != nil && tt.wants.err == nil) || (err == nil && tt.wants.err != nil) {
t.Fatalf("expected CompareAndSetPassword error %v got %v", tt.wants.err, err)
return
}
if err != nil {
if want, got := tt.wants.err.Error(), err.Error(); want != got {
t.Fatalf("expected CompareAndSetPassword error %v got %v", tt.wants.err, err)
}
return
}
})
}
}

View File

@ -41,6 +41,7 @@ var targetCmpOptions = cmp.Options{
func ScraperService(
init func(TargetFields, *testing.T) (platform.ScraperTargetStoreService, string, func()), t *testing.T,
) {
t.Helper()
tests := []struct {
name string
fn func(init func(TargetFields, *testing.T) (platform.ScraperTargetStoreService, string, func()),
@ -79,6 +80,7 @@ func AddTarget(
init func(TargetFields, *testing.T) (platform.ScraperTargetStoreService, string, func()),
t *testing.T,
) {
t.Helper()
type args struct {
userID platform.ID
target *platform.ScraperTarget
@ -160,7 +162,7 @@ func AddTarget(
wants: wants{
err: &platform.Error{
Code: platform.EInvalid,
Msg: "org id is invalid",
Msg: "provided organization ID has invalid format",
Op: platform.OpAddTarget,
},
userResourceMappings: []*platform.UserResourceMapping{},
@ -204,7 +206,7 @@ func AddTarget(
wants: wants{
err: &platform.Error{
Code: platform.EInvalid,
Msg: "bucket id is invalid",
Msg: "provided bucket ID has invalid format",
Op: platform.OpAddTarget,
},
userResourceMappings: []*platform.UserResourceMapping{},
@ -401,6 +403,7 @@ func GetTargetByID(
init func(TargetFields, *testing.T) (platform.ScraperTargetStoreService, string, func()),
t *testing.T,
) {
t.Helper()
type args struct {
id platform.ID
}
@ -706,7 +709,7 @@ func UpdateTarget(
err: &platform.Error{
Code: platform.EInvalid,
Op: platform.OpUpdateTarget,
Msg: "id is invalid",
Msg: "provided scraper target ID has invalid format",
},
},
},

View File

@ -70,10 +70,6 @@ func TelegrafConfigStore(
name: "FindTelegrafConfigByID",
fn: FindTelegrafConfigByID,
},
{
name: "FindTelegrafConfig",
fn: FindTelegrafConfig,
},
{
name: "FindTelegrafConfigs",
fn: FindTelegrafConfigs,
@ -415,10 +411,7 @@ func FindTelegrafConfigByID(
id: platform.ID(0),
},
wants: wants{
err: &platform.Error{
Code: platform.EEmptyValue,
Err: platform.ErrInvalidID,
},
err: fmt.Errorf("<invalid> provided telegraf configuration ID has invalid format"),
},
},
{
@ -460,7 +453,7 @@ func FindTelegrafConfigByID(
wants: wants{
err: &platform.Error{
Code: platform.ENotFound,
Msg: fmt.Sprintf("telegraf config with ID %v not found", MustIDBase16(threeID)),
Msg: "telegraf configuration not found",
},
},
},
@ -533,188 +526,8 @@ func FindTelegrafConfigByID(
}
if err != nil && tt.wants.err != nil {
if platform.ErrorCode(err) != platform.ErrorCode(tt.wants.err) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
}
}
if diff := cmp.Diff(tc, tt.wants.telegrafConfig, telegrafCmpOptions...); diff != "" {
t.Errorf("telegraf configs are different -got/+want\ndiff %s", diff)
}
})
}
}
// FindTelegrafConfig testing
func FindTelegrafConfig(
init func(TelegrafConfigFields, *testing.T) (platform.TelegrafConfigStore, func()),
t *testing.T,
) {
type args struct {
filter platform.TelegrafConfigFilter
}
type wants struct {
telegrafConfig *platform.TelegrafConfig
err error
}
tests := []struct {
name string
fields TelegrafConfigFields
args args
wants wants
}{
{
name: "find telegraf config",
fields: TelegrafConfigFields{
UserResourceMappings: []*platform.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
ResourceType: platform.TelegrafsResourceType,
UserID: MustIDBase16(threeID),
UserType: platform.Owner,
},
{
ResourceID: MustIDBase16(twoID),
ResourceType: platform.TelegrafsResourceType,
UserID: MustIDBase16(threeID),
UserType: platform.Member,
},
},
TelegrafConfigs: []*platform.TelegrafConfig{
{
ID: MustIDBase16(oneID),
OrganizationID: MustIDBase16(fourID),
Name: "tc1",
Plugins: []platform.TelegrafPlugin{
{
Config: &inputs.CPUStats{},
},
},
},
{
ID: MustIDBase16(twoID),
OrganizationID: MustIDBase16(fourID),
Name: "tc2",
Plugins: []platform.TelegrafPlugin{
{
Comment: "comment1",
Config: &inputs.File{
Files: []string{"f1", "f2"},
},
},
{
Comment: "comment2",
Config: &inputs.MemStats{},
},
},
},
},
},
args: args{
filter: platform.TelegrafConfigFilter{
UserResourceMappingFilter: platform.UserResourceMappingFilter{
UserID: MustIDBase16(threeID),
ResourceType: platform.TelegrafsResourceType,
UserType: platform.Member,
},
},
},
wants: wants{
telegrafConfig: &platform.TelegrafConfig{
ID: MustIDBase16(twoID),
OrganizationID: MustIDBase16(fourID),
Name: "tc2",
Plugins: []platform.TelegrafPlugin{
{
Comment: "comment1",
Config: &inputs.File{
Files: []string{"f1", "f2"},
},
},
{
Comment: "comment2",
Config: &inputs.MemStats{},
},
},
},
},
},
{
name: "find nothing",
fields: TelegrafConfigFields{
UserResourceMappings: []*platform.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
ResourceType: platform.TelegrafsResourceType,
UserID: MustIDBase16(threeID),
UserType: platform.Owner,
},
{
ResourceID: MustIDBase16(twoID),
ResourceType: platform.TelegrafsResourceType,
UserID: MustIDBase16(threeID),
UserType: platform.Member,
},
},
TelegrafConfigs: []*platform.TelegrafConfig{
{
ID: MustIDBase16(oneID),
OrganizationID: MustIDBase16(fourID),
Name: "tc1",
Plugins: []platform.TelegrafPlugin{
{
Config: &inputs.CPUStats{},
},
},
},
{
ID: MustIDBase16(twoID),
OrganizationID: MustIDBase16(fourID),
Name: "tc2",
Plugins: []platform.TelegrafPlugin{
{
Comment: "comment1",
Config: &inputs.File{
Files: []string{"f1", "f2"},
},
},
{
Comment: "comment2",
Config: &inputs.MemStats{},
},
},
},
},
},
args: args{
filter: platform.TelegrafConfigFilter{
UserResourceMappingFilter: platform.UserResourceMappingFilter{
UserID: MustIDBase16(fourID),
ResourceType: platform.TelegrafsResourceType,
},
},
},
wants: wants{
err: &platform.Error{
Code: platform.ENotFound,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.Background()
tc, err := s.FindTelegrafConfig(ctx, tt.args.filter)
if err != nil && tt.wants.err == nil {
t.Fatalf("expected errors to be nil got '%v'", err)
}
if err != nil && tt.wants.err != nil {
if platform.ErrorCode(err) != platform.ErrorCode(tt.wants.err) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
if want, got := tt.wants.err.Error(), err.Error(); want != got {
t.Fatalf("expected error '%s' got '%s'", want, got)
}
}
if diff := cmp.Diff(tc, tt.wants.telegrafConfig, telegrafCmpOptions...); diff != "" {
@ -1574,10 +1387,7 @@ func DeleteTelegrafConfig(
userID: MustIDBase16(threeID),
},
wants: wants{
err: &platform.Error{
Code: platform.EEmptyValue,
Err: platform.ErrInvalidID,
},
err: fmt.Errorf("<invalid> provided telegraf configuration ID has invalid format"),
userResourceMappings: []*platform.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
@ -1675,9 +1485,7 @@ func DeleteTelegrafConfig(
userID: MustIDBase16(threeID),
},
wants: wants{
err: &platform.Error{
Code: platform.ENotFound,
},
err: fmt.Errorf("<not found> telegraf configuration not found"),
userResourceMappings: []*platform.UserResourceMapping{
{
ResourceID: MustIDBase16(oneID),
@ -1809,10 +1617,11 @@ func DeleteTelegrafConfig(
}
if err != nil && tt.wants.err != nil {
if platform.ErrorCode(err) != platform.ErrorCode(tt.wants.err) {
if want, got := tt.wants.err.Error(), err.Error(); want != got {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
}
}
filter := platform.TelegrafConfigFilter{
UserResourceMappingFilter: platform.UserResourceMappingFilter{
UserID: tt.args.userID,
@ -1825,10 +1634,11 @@ func DeleteTelegrafConfig(
}
if err != nil && tt.wants.err != nil {
if platform.ErrorCode(err) != platform.ErrorCode(tt.wants.err) {
if want, got := tt.wants.err.Error(), err.Error(); want != got {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
}
}
if n != len(tt.wants.telegrafConfigs) {
t.Fatalf("telegraf configs length is different got %d, want %d", n, len(tt.wants.telegrafConfigs))
}

View File

@ -144,7 +144,7 @@ func CreateUserResourceMapping(
UserType: platform.Member,
},
},
err: fmt.Errorf("mapping for user %s already exists", userOneID),
err: fmt.Errorf("<internal error> Unexpected error when assigning user to a resource: mapping for user %s already exists", userOneID),
},
},
}
@ -226,7 +226,7 @@ func DeleteUserResourceMapping(
},
wants: wants{
mappings: []*platform.UserResourceMapping{},
err: fmt.Errorf("userResource mapping not found"),
err: fmt.Errorf("<not found> user to resource mapping not found"),
},
},
}

View File

@ -704,6 +704,27 @@ func FindUser(
},
},
},
{
name: "filter with no name nor id returns error",
fields: UserFields{
Users: []*platform.User{
{
ID: MustIDBase16(userTwoID),
Name: "xyz",
},
},
},
args: args{
filter: platform.UserFilter{},
},
wants: wants{
err: &platform.Error{
Code: platform.ENotFound,
Msg: "user not found",
Op: platform.OpFindUser,
},
},
},
{
name: "filter both name and non-existent id returns no user",
fields: UserFields{

View File

@ -8,11 +8,13 @@ import (
// TODO(goller): remove opPrefix argument
func diffPlatformErrors(name string, actual, expected error, opPrefix string, t *testing.T) {
t.Helper()
ErrorsEqual(t, actual, expected)
}
// ErrorsEqual checks to see if the provided errors are equivalent.
func ErrorsEqual(t *testing.T, actual, expected error) {
t.Helper()
if expected == nil && actual == nil {
return
}
@ -26,10 +28,12 @@ func ErrorsEqual(t *testing.T, actual, expected error) {
}
if platform.ErrorCode(expected) != platform.ErrorCode(actual) {
t.Logf("\nexpected: %v\nactual %v\n\n", expected, actual)
t.Errorf("expected error code %q but received %q", platform.ErrorCode(expected), platform.ErrorCode(actual))
}
if platform.ErrorMessage(expected) != platform.ErrorMessage(actual) {
t.Logf("\nexpected: %v\nactual %v\n\n", expected, actual)
t.Errorf("expected error message %q but received %q", platform.ErrorMessage(expected), platform.ErrorMessage(actual))
}
}

Some files were not shown because too many files have changed in this diff Show More