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 resourcespull/11959/head
parent
a76a2afbf8
commit
6a8a1fce32
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
19
Makefile
19
Makefile
|
@ -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
|
||||
|
|
15
README.md
15
README.md
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
38
bolt/kv.go
38
bolt/kv.go
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/bbolt"
|
||||
bolt "github.com/coreos/bbolt"
|
||||
platform "github.com/influxdata/influxdb"
|
||||
)
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
32
bolt/user.go
32
bolt/user.go
|
@ -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 {
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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("", ""),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
28
inmem/kv.go
28
inmem/kv.go
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
443
kv/example.go
443
kv/example.go
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
kv/store.go
15
kv/store.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue