diff --git a/.circleci/config.yml b/.circleci/config.yml index 49568b27c4..ebe6d8ce2a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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: diff --git a/.gitignore b/.gitignore index d6471cdb6e..f008afbd9b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile index 315a0ab752..f54906afcb 100644 --- a/Makefile +++ b/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 diff --git a/README.md b/README.md index 653c879bf5..4d56dab9fd 100644 --- a/README.md +++ b/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`. diff --git a/authorizer/telegraf.go b/authorizer/telegraf.go index 8c67b2cac2..43b81b6c28 100644 --- a/authorizer/telegraf.go +++ b/authorizer/telegraf.go @@ -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 diff --git a/authorizer/telegraf_test.go b/authorizer/telegraf_test.go index ebd0f4e3c1..0b149bfa5e 100644 --- a/authorizer/telegraf_test.go +++ b/authorizer/telegraf_test.go @@ -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 diff --git a/bolt/keyvalue_log.go b/bolt/keyvalue_log.go index 0566fb68f0..e8bec07b9a 100644 --- a/bolt/keyvalue_log.go +++ b/bolt/keyvalue_log.go @@ -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) diff --git a/bolt/kv.go b/bolt/kv.go index abfc06914c..512e1c0fea 100644 --- a/bolt/kv.go +++ b/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 diff --git a/bolt/kv_test.go b/bolt/kv_test.go index 871ca92a6f..a9cbeff979 100644 --- a/bolt/kv_test.go +++ b/bolt/kv_test.go @@ -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) -} diff --git a/bolt/metrics_test.go b/bolt/metrics_test.go index 9c3ce99dc3..ed18104a5b 100644 --- a/bolt/metrics_test.go +++ b/bolt/metrics_test.go @@ -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 { diff --git a/bolt/onboarding.go b/bolt/onboarding.go index 4b704dff19..65f1da2fb2 100644 --- a/bolt/onboarding.go +++ b/bolt/onboarding.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/coreos/bbolt" + bolt "github.com/coreos/bbolt" platform "github.com/influxdata/influxdb" ) diff --git a/bolt/basic_auth_service.go b/bolt/passwords.go similarity index 58% rename from bolt/basic_auth_service.go rename to bolt/passwords.go index 39592b260c..10977077ab 100644 --- a/bolt/basic_auth_service.go +++ b/bolt/passwords.go @@ -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: " your username or password is incorrect", + } + + // EShortPassword is used when a password is less than the minimum + // acceptable password length. + EShortPassword = &platform.Error{ + Msg: " 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. diff --git a/bolt/basic_auth_service_test.go b/bolt/passwords_test.go similarity index 55% rename from bolt/basic_auth_service_test.go rename to bolt/passwords_test.go index 8887cb427f..1aeab2587d 100644 --- a/bolt/basic_auth_service_test.go +++ b/bolt/passwords_test.go @@ -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) } diff --git a/bolt/scraper.go b/bolt/scraper.go index 2cd426f921..6e85e9efd6 100644 --- a/bolt/scraper.go +++ b/bolt/scraper.go @@ -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 { diff --git a/bolt/telegraf.go b/bolt/telegraf.go index c4e7ba4e69..fc8618cff9 100644 --- a/bolt/telegraf.go +++ b/bolt/telegraf.go @@ -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, } } diff --git a/bolt/user.go b/bolt/user.go index a14660f064..502193e317 100644 --- a/bolt/user.go +++ b/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 { diff --git a/bolt/user_resource_mapping.go b/bolt/user_resource_mapping.go index 53cce971a8..b8ae8ab47e 100644 --- a/bolt/user_resource_mapping.go +++ b/bolt/user_resource_mapping.go @@ -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]) diff --git a/bolt/user_test.go b/bolt/user_test.go index bc22f5681a..4ef23f9fd4 100644 --- a/bolt/user_test.go +++ b/bolt/user_test.go @@ -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) } diff --git a/cmd/influx/main.go b/cmd/influx/main.go index 3e34cb0346..7f6ec11447 100644 --- a/cmd/influx/main.go +++ b/cmd/influx/main.go @@ -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 } diff --git a/cmd/influxd/launcher/launcher.go b/cmd/influxd/launcher/launcher.go index 71308e569a..fa881e0aa4 100644 --- a/cmd/influxd/launcher/launcher.go +++ b/cmd/influxd/launcher/launcher.go @@ -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 } diff --git a/etc/pinger.sh b/etc/pinger.sh new file mode 100755 index 0000000000..2a13195dce --- /dev/null +++ b/etc/pinger.sh @@ -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" diff --git a/http/api_handler.go b/http/api_handler.go index 40825d4015..7b5d0d81be 100644 --- a/http/api_handler.go +++ b/http/api_handler.go @@ -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 diff --git a/http/assets.go b/http/assets.go index de03388706..d475be837b 100644 --- a/http/assets.go +++ b/http/assets.go @@ -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{ diff --git a/http/debug.go b/http/debug.go new file mode 100644 index 0000000000..54c2aa4174 --- /dev/null +++ b/http/debug.go @@ -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) + }) +} diff --git a/http/onboarding_test.go b/http/onboarding_test.go index d7cd5b08dc..4fd0dd2165 100644 --- a/http/onboarding_test.go +++ b/http/onboarding_test.go @@ -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 diff --git a/http/platform_handler.go b/http/platform_handler.go index ae71719b53..91db0e98ec 100644 --- a/http/platform_handler.go +++ b/http/platform_handler.go @@ -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, diff --git a/http/scraper_service.go b/http/scraper_service.go index ae3d4719a6..1808a38c07 100644 --- a/http/scraper_service.go +++ b/http/scraper_service.go @@ -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, } } diff --git a/http/session_handler.go b/http/session_handler.go index a671385a39..0c03acde17 100644 --- a/http/session_handler.go +++ b/http/session_handler.go @@ -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 diff --git a/http/session_test.go b/http/session_test.go index f3924f8957..23f0e90205 100644 --- a/http/session_test.go +++ b/http/session_test.go @@ -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) diff --git a/http/swagger.yml b/http/swagger.yml index 1ebd9101bb..29567158b2 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -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 diff --git a/http/user_service.go b/http/user_service.go index e11f974a16..03e14fe982 100644 --- a/http/user_service.go +++ b/http/user_service.go @@ -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)) diff --git a/http/user_test.go b/http/user_test.go index e872e7eeeb..1a96aefa92 100644 --- a/http/user_test.go +++ b/http/user_test.go @@ -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("", ""), } } diff --git a/inmem/basic_auth_test.go b/inmem/basic_auth_test.go deleted file mode 100644 index 1e800f3ea6..0000000000 --- a/inmem/basic_auth_test.go +++ /dev/null @@ -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) -} diff --git a/inmem/kv.go b/inmem/kv.go index a4a40b7fd5..97bfffd2df 100644 --- a/inmem/kv.go +++ b/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 { diff --git a/inmem/kv_test.go b/inmem/kv_test.go index c154831817..3a40939ddc 100644 --- a/inmem/kv_test.go +++ b/inmem/kv_test.go @@ -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) + } + }) + } +} diff --git a/inmem/basic_auth.go b/inmem/passwords.go similarity index 58% rename from inmem/basic_auth.go rename to inmem/passwords.go index f14013152e..1aba40eca5 100644 --- a/inmem/basic_auth.go +++ b/inmem/passwords.go @@ -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: " your username or password is incorrect", + } + + // EShortPassword is used when a password is less than the minimum + // acceptable password length. + EShortPassword = &platform.Error{ + Msg: " 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(" your username or password is incorrect") + } + return nil } // CompareAndSetPassword replaces the old password with the new password if thee old password is correct. diff --git a/inmem/passwords_test.go b/inmem/passwords_test.go new file mode 100644 index 0000000000..6a1fe96999 --- /dev/null +++ b/inmem/passwords_test.go @@ -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) +} diff --git a/inmem/scraper.go b/inmem/scraper.go index ad5eec09d7..deeed7c8c1 100644 --- a/inmem/scraper.go +++ b/inmem/scraper.go @@ -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) diff --git a/inmem/service.go b/inmem/service.go index df760d70cf..3d0099c86a 100644 --- a/inmem/service.go +++ b/inmem/service.go @@ -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) + } + +} diff --git a/inmem/session.go b/inmem/session.go new file mode 100644 index 0000000000..241693122b --- /dev/null +++ b/inmem/session.go @@ -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 +} diff --git a/inmem/source.go b/inmem/source.go new file mode 100644 index 0000000000..bbf03e5f9f --- /dev/null +++ b/inmem/source.go @@ -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 +} diff --git a/inmem/telegraf.go b/inmem/telegraf.go index 30c21b9bc0..395b6df88b 100644 --- a/inmem/telegraf.go +++ b/inmem/telegraf.go @@ -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 { diff --git a/inmem/user_resource_mapping_service.go b/inmem/user_resource_mapping_service.go index ee70b0a610..ba70b89591 100644 --- a/inmem/user_resource_mapping_service.go +++ b/inmem/user_resource_mapping_service.go @@ -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) diff --git a/inmem/user_test.go b/inmem/user_test.go index f86839c1c3..4595239a1e 100644 --- a/inmem/user_test.go +++ b/inmem/user_test.go @@ -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) } diff --git a/keyvalue_log.go b/keyvalue_log.go index 6f5d635255..7e59e8a4bc 100644 --- a/keyvalue_log.go +++ b/keyvalue_log.go @@ -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: diff --git a/kv/auth.go b/kv/auth.go new file mode 100644 index 0000000000..fbcce2a7ed --- /dev/null +++ b/kv/auth.go @@ -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 +} diff --git a/kv/auth_test.go b/kv/auth_test.go new file mode 100644 index 0000000000..0073cc05e6 --- /dev/null +++ b/kv/auth_test.go @@ -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) + } + } + } +} diff --git a/kv/bucket.go b/kv/bucket.go new file mode 100644 index 0000000000..48eaa9ce0a --- /dev/null +++ b/kv/bucket.go @@ -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), + } +} diff --git a/kv/bucket_test.go b/kv/bucket_test.go new file mode 100644 index 0000000000..2a95bd2cbf --- /dev/null +++ b/kv/bucket_test.go @@ -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) + } + } + } +} diff --git a/kv/dashboard.go b/kv/dashboard.go new file mode 100644 index 0000000000..f8df5e2b77 --- /dev/null +++ b/kv/dashboard.go @@ -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()) +} diff --git a/kv/dashboard_test.go b/kv/dashboard_test.go new file mode 100644 index 0000000000..b2ea4e5376 --- /dev/null +++ b/kv/dashboard_test.go @@ -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) + } + } + } +} diff --git a/kv/example.go b/kv/example.go deleted file mode 100644 index 56508be9c0..0000000000 --- a/kv/example.go +++ /dev/null @@ -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 -} diff --git a/kv/kv_test.go b/kv/kv_test.go new file mode 100644 index 0000000000..9540626d8d --- /dev/null +++ b/kv/kv_test.go @@ -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 +} diff --git a/kv/kvlog.go b/kv/kvlog.go new file mode 100644 index 0000000000..74225f11d8 --- /dev/null +++ b/kv/kvlog.go @@ -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()) +} diff --git a/kv/kvlog_test.go b/kv/kvlog_test.go new file mode 100644 index 0000000000..ce2ee63c34 --- /dev/null +++ b/kv/kvlog_test.go @@ -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() { + } +} diff --git a/kv/label.go b/kv/label.go new file mode 100644 index 0000000000..e742437d69 --- /dev/null +++ b/kv/label.go @@ -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) +} diff --git a/kv/label_test.go b/kv/label_test.go new file mode 100644 index 0000000000..6dd87e1481 --- /dev/null +++ b/kv/label_test.go @@ -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) + } + } + } +} diff --git a/kv/lookup_service.go b/kv/lookup_service.go new file mode 100644 index 0000000000..29a1a9a880 --- /dev/null +++ b/kv/lookup_service.go @@ -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 +} diff --git a/kv/lookup_service_test.go b/kv/lookup_service_test.go new file mode 100644 index 0000000000..fe53175b68 --- /dev/null +++ b/kv/lookup_service_test.go @@ -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) + } + }) + } +} diff --git a/kv/onboarding.go b/kv/onboarding.go new file mode 100644 index 0000000000..d7f070ad8b --- /dev/null +++ b/kv/onboarding.go @@ -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 +} diff --git a/kv/onboarding_test.go b/kv/onboarding_test.go new file mode 100644 index 0000000000..c784fa0d61 --- /dev/null +++ b/kv/onboarding_test.go @@ -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) + } + } +} diff --git a/kv/org.go b/kv/org.go new file mode 100644 index 0000000000..111bb7ceeb --- /dev/null +++ b/kv/org.go @@ -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), + } +} diff --git a/kv/org_test.go b/kv/org_test.go new file mode 100644 index 0000000000..c592dd6cc7 --- /dev/null +++ b/kv/org_test.go @@ -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) + } + } + } +} diff --git a/kv/passwords.go b/kv/passwords.go new file mode 100644 index 0000000000..5b738c5b62 --- /dev/null +++ b/kv/passwords.go @@ -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) +} diff --git a/kv/passwords_test.go b/kv/passwords_test.go new file mode 100644 index 0000000000..f54701c280 --- /dev/null +++ b/kv/passwords_test.go @@ -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(" 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(" 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: 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: 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: 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: 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(" 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: 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: 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(" 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) + } + } + }) + } +} diff --git a/kv/scrapers.go b/kv/scrapers.go new file mode 100644 index 0000000000..5cbc469902 --- /dev/null +++ b/kv/scrapers.go @@ -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 +} diff --git a/kv/scrapers_test.go b/kv/scrapers_test.go new file mode 100644 index 0000000000..13bf04ad07 --- /dev/null +++ b/kv/scrapers_test.go @@ -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) + } + } + } +} diff --git a/kv/secret.go b/kv/secret.go new file mode 100644 index 0000000000..124f61ebb9 --- /dev/null +++ b/kv/secret.go @@ -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) +} diff --git a/kv/secret_test.go b/kv/secret_test.go new file mode 100644 index 0000000000..9db271a072 --- /dev/null +++ b/kv/secret_test.go @@ -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() {} +} diff --git a/kv/service.go b/kv/service.go new file mode 100644 index 0000000000..5f6747fbf9 --- /dev/null +++ b/kv/service.go @@ -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 +} diff --git a/kv/session.go b/kv/session.go new file mode 100644 index 0000000000..b052730826 --- /dev/null +++ b/kv/session.go @@ -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 +} diff --git a/kv/session_test.go b/kv/session_test.go new file mode 100644 index 0000000000..9f9993ae14 --- /dev/null +++ b/kv/session_test.go @@ -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) + } + } + } +} diff --git a/kv/source.go b/kv/source.go new file mode 100644 index 0000000000..40c663b902 --- /dev/null +++ b/kv/source.go @@ -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 +} diff --git a/kv/source_test.go b/kv/source_test.go new file mode 100644 index 0000000000..20ec4ad27c --- /dev/null +++ b/kv/source_test.go @@ -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) + } + } + } +} diff --git a/kv/store.go b/kv/store.go index cab2ce799e..d2c1103f58 100644 --- a/kv/store.go +++ b/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) } diff --git a/kv/telegraf.go b/kv/telegraf.go new file mode 100644 index 0000000000..7c2a3ff0c1 --- /dev/null +++ b/kv/telegraf.go @@ -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 +} diff --git a/kv/telegraf_test.go b/kv/telegraf_test.go new file mode 100644 index 0000000000..bb4b5292c1 --- /dev/null +++ b/kv/telegraf_test.go @@ -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) + } + } + } +} diff --git a/kv/unique.go b/kv/unique.go new file mode 100644 index 0000000000..ba1818515f --- /dev/null +++ b/kv/unique.go @@ -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) +} diff --git a/kv/urm.go b/kv/urm.go new file mode 100644 index 0000000000..b4e0e41106 --- /dev/null +++ b/kv/urm.go @@ -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 +} diff --git a/kv/urm_test.go b/kv/urm_test.go new file mode 100644 index 0000000000..8266cb243f --- /dev/null +++ b/kv/urm_test.go @@ -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) + } + } + } +} diff --git a/kv/user.go b/kv/user.go new file mode 100644 index 0000000000..b77f05c3b2 --- /dev/null +++ b/kv/user.go @@ -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", + } +} diff --git a/kv/user_test.go b/kv/user_test.go new file mode 100644 index 0000000000..c9a0c5a1ca --- /dev/null +++ b/kv/user_test.go @@ -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) + } + } + } +} diff --git a/kv/variable.go b/kv/variable.go new file mode 100644 index 0000000000..4ac8cd0b5f --- /dev/null +++ b/kv/variable.go @@ -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 + }) +} diff --git a/kv/variable_test.go b/kv/variable_test.go new file mode 100644 index 0000000000..0e3355de53 --- /dev/null +++ b/kv/variable_test.go @@ -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 +} diff --git a/mock/kv.go b/mock/kv.go new file mode 100644 index 0000000000..1d2b7b1209 --- /dev/null +++ b/mock/kv.go @@ -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() +} diff --git a/mock/onboarding_service.go b/mock/onboarding_service.go index da6afe9ec7..c602ef9dc1 100644 --- a/mock/onboarding_service.go +++ b/mock/onboarding_service.go @@ -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 diff --git a/mock/basic_auth.go b/mock/passwords.go similarity index 67% rename from mock/basic_auth.go rename to mock/passwords.go index 8738c17d36..8341505a02 100644 --- a/mock/basic_auth.go +++ b/mock/passwords.go @@ -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) } diff --git a/mock/telegraf_service.go b/mock/telegraf_service.go index 122534908b..c724c87f9a 100644 --- a/mock/telegraf_service.go +++ b/mock/telegraf_service.go @@ -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) { diff --git a/onboarding.go b/onboarding.go index 16286ed278..703f5ff2db 100644 --- a/onboarding.go +++ b/onboarding.go @@ -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 } diff --git a/passwords.go b/passwords.go new file mode 100644 index 0000000000..55e2c3db97 --- /dev/null +++ b/passwords.go @@ -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 +} diff --git a/task/validator_test.go b/task/validator_test.go index 47433ca7eb..4c889690a4 100644 --- a/task/validator_test.go +++ b/task/validator_test.go @@ -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, diff --git a/telegraf.go b/telegraf.go index 71c8fa960d..384a0ec09b 100644 --- a/telegraf.go +++ b/telegraf.go @@ -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) diff --git a/testing/basic_auth.go b/testing/basic_auth.go deleted file mode 100644 index 45460d6bb9..0000000000 --- a/testing/basic_auth.go +++ /dev/null @@ -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 - } - - }) - } - -} diff --git a/testing/onboarding.go b/testing/onboarding.go index 4302fd81f5..edf1414d74 100644 --- a/testing/onboarding.go +++ b/testing/onboarding.go @@ -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) } } diff --git a/testing/passwords.go b/testing/passwords.go new file mode 100644 index 0000000000..f7eb9c3fd0 --- /dev/null +++ b/testing/passwords.go @@ -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(" 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(" 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(" 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(" 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(" 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(" your username or password is incorrect"), + }, + }, + { + name: " 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(" 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 + } + + }) + } + +} diff --git a/testing/scraper_target.go b/testing/scraper_target.go index ed3cefdb0d..d86fe7af16 100644 --- a/testing/scraper_target.go +++ b/testing/scraper_target.go @@ -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", }, }, }, diff --git a/testing/telegraf.go b/testing/telegraf.go index d563ad0d48..acbff1a3c3 100644 --- a/testing/telegraf.go +++ b/testing/telegraf.go @@ -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(" 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(" 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(" 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)) } diff --git a/testing/user_resource_mapping_service.go b/testing/user_resource_mapping_service.go index a7c321c982..2e17dc05b5 100644 --- a/testing/user_resource_mapping_service.go +++ b/testing/user_resource_mapping_service.go @@ -144,7 +144,7 @@ func CreateUserResourceMapping( UserType: platform.Member, }, }, - err: fmt.Errorf("mapping for user %s already exists", userOneID), + err: fmt.Errorf(" 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(" user to resource mapping not found"), }, }, } diff --git a/testing/user_service.go b/testing/user_service.go index fa86fc551f..d5d42481b2 100644 --- a/testing/user_service.go +++ b/testing/user_service.go @@ -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{ diff --git a/testing/util.go b/testing/util.go index e979c09bca..005bf5e2f1 100644 --- a/testing/util.go +++ b/testing/util.go @@ -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)) } } diff --git a/ui/Makefile b/ui/Makefile index be3a98bca8..132c065f0f 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -3,7 +3,10 @@ UISOURCES := $(shell find . -type f -not \( -path ./build/\* -o -path ./node_mod all: build node_modules: package-lock.json - npm i + npm ci + + e2e: node_modules + npm run test:junit build: node_modules $(UISOURCES) npm run build @@ -20,4 +23,4 @@ clean: run: npm start -.PHONY: all clean test run lint +.PHONY: all clean test run lint junit diff --git a/ui/cypress.json b/ui/cypress.json new file mode 100644 index 0000000000..08389c4a9a --- /dev/null +++ b/ui/cypress.json @@ -0,0 +1,4 @@ +{ + "baseUrl": "http://localhost:9999", + "integrationFolder": "cypress/e2e" +} diff --git a/ui/cypress/e2e/buckets.ts b/ui/cypress/e2e/buckets.ts new file mode 100644 index 0000000000..ca864c6574 --- /dev/null +++ b/ui/cypress/e2e/buckets.ts @@ -0,0 +1,63 @@ +describe('Buckets', () => { + let orgID: string = '' + let bucketName: string = '' + beforeEach(() => { + cy.flush() + + cy.setupUser().then(({body}) => { + const {org, bucket} = body + orgID = org.id + bucketName = bucket.name + }) + + cy.signin() + + cy.fixture('routes').then(({orgs}) => { + cy.visit(`${orgs}/${orgID}/buckets_tab`) + }) + }) + + describe('from the org view', () => { + it('can create a bucket', () => { + const newBucket = '🅱️ucket' + cy.getByDataTest('table-row').should('have.length', 1) + + cy.contains('Create').click() + cy.getByDataTest('overlay--container').within(() => { + cy.getByInputName('name').type(newBucket) + cy.get('.button') + .contains('Create') + .click() + }) + + cy.getByDataTest('table-row') + .should('have.length', 2) + .and('contain', newBucket) + }) + + it('can update a buckets name and retention rules', () => { + const newName = 'newdefbuck' + + cy.contains(bucketName).click() + + cy.getByDataTest('retention-intervals').click() + + cy.getByInputName('days').type('{uparrow}') + cy.getByInputName('hours').type('{uparrow}') + cy.getByInputName('minutes').type('{uparrow}') + cy.getByInputName('seconds').type('{uparrow}') + + cy.getByDataTest('overlay--container').within(() => { + cy.getByInputName('name') + .clear() + .type(newName) + + cy.contains('Save').click() + }) + + cy.getByDataTest('table-row') + .should('contain', '1 day') + .and('contain', newName) + }) + }) +}) diff --git a/ui/cypress/e2e/dashboards.ts b/ui/cypress/e2e/dashboards.ts new file mode 100644 index 0000000000..2d5e6bc445 --- /dev/null +++ b/ui/cypress/e2e/dashboards.ts @@ -0,0 +1,80 @@ +describe('Dashboards', () => { + let orgID: string = '' + beforeEach(() => { + cy.flush() + + cy.setupUser().then(({body}) => { + orgID = body.org.id + }) + + cy.signin() + + cy.fixture('routes').then(({dashboards}) => { + cy.visit(dashboards) + }) + }) + + it('can create a dashboard from empty state', () => { + cy.get('.empty-state') + .contains('Create') + .click() + + cy.visit('/dashboards') + + cy.get('.index-list--row') + .its('length') + .should('be.eq', 1) + }) + + it('can create a dashboard from the header', () => { + cy.get('.page-header--container') + .contains('Create') + .click() + + cy.getByDataTest('dropdown--item New Dashboard').click() + + cy.visit('/dashboards') + + cy.get('.index-list--row') + .its('length') + .should('be.eq', 1) + }) + + it('can delete a dashboard', () => { + cy.createDashboard(orgID) + cy.createDashboard(orgID) + + cy.get('.index-list--row').then(rows => { + const numDashboards = rows.length + + cy.get('.button-danger') + .first() + .click() + + cy.contains('Confirm') + .first() + .click({force: true}) + + cy.get('.index-list--row') + .its('length') + .should('eq', numDashboards - 1) + }) + }) + + it('can edit a dashboards name', () => { + cy.createDashboard(orgID).then(({body}) => { + cy.visit(`/dashboards/${body.id}`) + }) + + const newName = 'new 🅱️ashboard' + + cy.get('.renamable-page-title--title').click() + cy.get('.input-field') + .type(newName) + .type('{enter}') + + cy.visit('/dashboards') + + cy.get('.index-list--row').should('contain', newName) + }) +}) diff --git a/ui/cypress/e2e/login.ts b/ui/cypress/e2e/login.ts new file mode 100644 index 0000000000..9acd261f58 --- /dev/null +++ b/ui/cypress/e2e/login.ts @@ -0,0 +1,59 @@ +interface LoginUser { + username: string + password: string +} + +describe('The Login Page', () => { + let user: LoginUser + beforeEach(() => { + cy.flush() + + cy.fixture('user').then(u => { + user = u + }) + + cy.setupUser() + + cy.visit('/') + }) + + it('can login', () => { + cy.getByInputName('username').type(user.username) + cy.getByInputName('password').type(user.password) + cy.get('button[type=submit]').click() + + cy.getByDataTest('nav').should('exist') + }) + + describe('login failure', () => { + it('if username is not present', () => { + cy.getByInputName('password').type(user.password) + cy.get('button[type=submit]').click() + + cy.getByDataTest('notification-error').should('exist') + }) + + it('if password is not present', () => { + cy.getByInputName('username').type(user.username) + cy.get('button[type=submit]').click() + + cy.getByDataTest('notification-error').should('exist') + }) + + it('if username is incorrect', () => { + cy.getByInputName('username').type('not-a-user') + cy.getByInputName('password').type(user.password) + cy.get('button[type=submit]').click() + + cy.getByDataTest('notification-error').should('exist') + }) + + it('if password is incorrect', () => { + cy.getByInputName('username').type(user.username) + cy.getByInputName('password').type('not-a-password') + cy.get('button[type=submit]').click() + + cy.getByDataTest('notification-error').should('exist') + }) + }) +}) diff --git a/ui/cypress/e2e/orgs.ts b/ui/cypress/e2e/orgs.ts new file mode 100644 index 0000000000..3fe3dff677 --- /dev/null +++ b/ui/cypress/e2e/orgs.ts @@ -0,0 +1,63 @@ +const orgRoute = '/organizations' + +describe('Orgs', () => { + beforeEach(() => { + cy.flush() + + cy.setupUser() + + cy.signin() + + cy.visit(orgRoute) + }) + + it('can create an org', () => { + cy.get('.index-list--row') + .its('length') + .should('be.eq', 1) + + cy.get('.page-header--right > .button') + .contains('Create') + .click() + + const orgName = '🅱️organization' + cy.getByInputName('name').type(orgName) + + cy.getByTitle('Create').click() + + cy.get('.index-list--row') + .should('contain', orgName) + .its('length') + .should('be.eq', 2) + }) + + it('can delete an org', () => { + cy.createOrg() + + cy.get('.index-list--row').then(rows => { + const numOrgs = rows.length + + cy.contains('Confirm').click({force: true}) + + cy.get('.index-list--row') + .its('length') + .should('eq', numOrgs - 1) + }) + }) + + it('can update an org name', () => { + cy.createOrg().then(({body}) => { + const newName = 'new 🅱️organization' + cy.visit(`${orgRoute}/${body.id}/member_tab`) + + cy.get('.renamable-page-title--title').click() + cy.get('.input-field') + .type(newName) + .type('{enter}') + + cy.visit('/organizations') + + cy.get('.index-list--row').should('contain', newName) + }) + }) +}) diff --git a/ui/cypress/e2e/tasks.ts b/ui/cypress/e2e/tasks.ts new file mode 100644 index 0000000000..f2e7edfccd --- /dev/null +++ b/ui/cypress/e2e/tasks.ts @@ -0,0 +1,31 @@ +// currently getting unauthorized errors for task creation +describe.skip('Tasks', () => { + let orgID: string = '' + beforeEach(() => { + cy.flush() + + cy.setupUser().then(({body}) => { + orgID = body.org.id + }) + + cy.signin() + + cy.visit('/tasks') + }) + + it('can create a task', () => { + cy.get('.empty-state').within(() => { + cy.contains('Create').click() + }) + + cy.getByInputName('name').type('🅱️ask') + cy.getByInputName('interval').type('1d') + cy.getByInputName('offset').type('20m') + + cy.getByDataTest('flux-editor').within(() => { + cy.get('textarea').type('{}', {force: true}) + }) + + cy.contains('Save').click() + }) +}) diff --git a/ui/cypress/fixtures/routes.json b/ui/cypress/fixtures/routes.json new file mode 100644 index 0000000000..a4ee318029 --- /dev/null +++ b/ui/cypress/fixtures/routes.json @@ -0,0 +1,4 @@ +{ + "orgs": "/organizations", + "dashboards": "/dashboards" +} diff --git a/ui/cypress/fixtures/user.json b/ui/cypress/fixtures/user.json new file mode 100644 index 0000000000..d83ac5e6a1 --- /dev/null +++ b/ui/cypress/fixtures/user.json @@ -0,0 +1,6 @@ +{ + "username": "u1", + "password": "password", + "org": "deforg", + "bucket": "defbuck" +} diff --git a/ui/cypress/index.d.ts b/ui/cypress/index.d.ts new file mode 100644 index 0000000000..5c5d9faf6a --- /dev/null +++ b/ui/cypress/index.d.ts @@ -0,0 +1,25 @@ +import { + signin, + setupUser, + createDashboard, + createOrg, + flush, + getByDataTest, + getByInputName, + getByTitle, +} from './support/commands' + +declare global { + namespace Cypress { + interface Chainable { + signin: typeof signin + setupUser: typeof setupUser + createDashboard: typeof createDashboard + createOrg: typeof createOrg + flush: typeof flush + getByDataTest: typeof getByDataTest + getByInputName: typeof getByInputName + getByTitle: typeof getByTitle + } + } +} diff --git a/ui/cypress/plugins/index.js b/ui/cypress/plugins/index.js new file mode 100644 index 0000000000..f67468ae04 --- /dev/null +++ b/ui/cypress/plugins/index.js @@ -0,0 +1,11 @@ +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) +const wp = require('@cypress/webpack-preprocessor') + +module.exports = on => { + const options = { + webpackOptions: require('../webpack.config.js') + } + + on('file:preprocessor', wp(options)) +} diff --git a/ui/cypress/support/commands.ts b/ui/cypress/support/commands.ts new file mode 100644 index 0000000000..fa15a539a9 --- /dev/null +++ b/ui/cypress/support/commands.ts @@ -0,0 +1,97 @@ +export const signin = (): Cypress.Chainable => { + return cy.fixture('user').then(user => { + cy.request({ + method: 'POST', + url: '/api/v2/signin', + auth: {user: user.username, pass: user.password}, + }) + }) +} + +// createDashboard relies on an org fixture to be set +export const createDashboard = ( + orgID: string +): Cypress.Chainable => { + return cy.request({ + method: 'POST', + url: '/api/v2/dashboards', + body: { + name: 'test dashboard', + orgID, + }, + }) +} + +export const createOrg = (): Cypress.Chainable => { + return cy.request({ + method: 'POST', + url: '/api/v2/orgs', + body: { + name: 'test org', + }, + }) +} + +export const createBucket = (): Cypress.Chainable => { + return cy.request({ + method: 'POST', + url: '/api/v2/buckets', + body: { + name: 'test org', + }, + }) +} + +// TODO: have to go through setup because we cannot create a user w/ a password via the user API +export const setupUser = (): Cypress.Chainable => { + return cy.fixture('user').then(({username, password, org, bucket}) => { + return cy.request({ + method: 'POST', + url: '/api/v2/setup', + body: {username, password, org, bucket}, + }) + }) +} + +export const flush = () => { + cy.request({ + method: 'GET', + url: '/debug/flush', + }) +} + +// DOM node getters +export const getByDataTest = (dataTest: string): Cypress.Chainable => { + return cy.get(`[data-testid="${dataTest}"]`) +} + +export const getByInputName = (name: string): Cypress.Chainable => { + return cy.get(`input[name=${name}]`) +} + +export const getByTitle = (name: string): Cypress.Chainable => { + return cy.get(`[title=${name}]`) +} + +// getters +Cypress.Commands.add('getByDataTest', getByDataTest) +Cypress.Commands.add('getByInputName', getByInputName) +Cypress.Commands.add('getByTitle', getByTitle) + +// auth flow +Cypress.Commands.add('signin', signin) + +// setup +Cypress.Commands.add('setupUser', setupUser) + +// dashboards +Cypress.Commands.add('createDashboard', createDashboard) + +// orgs +Cypress.Commands.add('createOrg', createOrg) + +// buckets +Cypress.Commands.add('createBucket', createBucket) + +// general +Cypress.Commands.add('flush', flush) diff --git a/ui/cypress/support/index.js b/ui/cypress/support/index.js new file mode 100644 index 0000000000..d68db96df2 --- /dev/null +++ b/ui/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/ui/cypress/tsconfig.json b/ui/cypress/tsconfig.json new file mode 100644 index 0000000000..c52feb9175 --- /dev/null +++ b/ui/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "baseUrl": "../node_modules", + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "mocha", "node"] + }, + "include": ["**/*.ts"] +} diff --git a/ui/cypress/webpack.config.js b/ui/cypress/webpack.config.js new file mode 100644 index 0000000000..d898c51582 --- /dev/null +++ b/ui/cypress/webpack.config.js @@ -0,0 +1,18 @@ +module.exports = { + resolve: { + extensions: ['.ts', '.js'] + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: [/node_modules/], + use: [ + { + loader: 'ts-loader' + } + ] + } + ] + } +} diff --git a/ui/package-lock.json b/ui/package-lock.json index 3b958e7b9f..b970ba8cb0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -277,14 +277,27 @@ } }, "@babel/helpers": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.2.0.tgz", - "integrity": "sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.3.1.tgz", + "integrity": "sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==", "dev": true, "requires": { "@babel/template": "^7.1.2", "@babel/traverse": "^7.1.5", - "@babel/types": "^7.2.0" + "@babel/types": "^7.3.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", + "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.10", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/highlight": { @@ -326,9 +339,9 @@ } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.2.0.tgz", - "integrity": "sha512-1L5mWLSvR76XYUQJXkd/EEQgjq8HHRP6lQuZTTg0VA4tTGPpGemmCdAfQIz1rzEuWAm+ecP8PyyEm30jC1eQCg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.1.tgz", + "integrity": "sha512-Nmmv1+3LqxJu/V5jU9vJmxR/KIRWFk2qLHmbB56yRRRFhlaSuOVXscX3gUmhaKgUhzA3otOHVubbIEVYsZ0eZg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", @@ -592,6 +605,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz", + "integrity": "sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw==", + "dev": true, + "requires": { + "regexp-tree": "^0.1.0" + } + }, "@babel/plugin-transform-new-target": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz", @@ -701,19 +723,20 @@ } }, "@babel/preset-env": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.2.3.tgz", - "integrity": "sha512-AuHzW7a9rbv5WXmvGaPX7wADxFkZIqKlbBh1dmZUQp4iwiPpkE/Qnrji6SC4UQCQzvWY/cpHET29eUhXS9cLPw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.3.1.tgz", + "integrity": "sha512-FHKrD6Dxf30e8xgHQO0zJZpUPfVZg+Xwgz5/RdSWCbza9QLNk4Qbp40ctRoqDxml3O8RMzB1DU55SXeDG6PqHQ==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-proposal-async-generator-functions": "^7.2.0", "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-object-rest-spread": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.3.1", "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", "@babel/plugin-proposal-unicode-property-regex": "^7.2.0", "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", "@babel/plugin-syntax-object-rest-spread": "^7.2.0", "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", "@babel/plugin-transform-arrow-functions": "^7.2.0", @@ -733,6 +756,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.2.0", "@babel/plugin-transform-modules-systemjs": "^7.2.0", "@babel/plugin-transform-modules-umd": "^7.2.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.3.0", "@babel/plugin-transform-new-target": "^7.0.0", "@babel/plugin-transform-object-super": "^7.2.0", "@babel/plugin-transform-parameters": "^7.2.0", @@ -822,6 +846,131 @@ "to-fast-properties": "^2.0.0" } }, + "@cypress/listr-verbose-renderer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", + "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cli-cursor": "^1.0.2", + "date-fns": "^1.27.2", + "figures": "^1.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "@cypress/webpack-preprocessor": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@cypress/webpack-preprocessor/-/webpack-preprocessor-4.0.3.tgz", + "integrity": "sha512-gw6QNif0UaMW1FDl5tej14isvDWbONib9t1iXGlWUaz0/pEdIvp6ik7mnOaph/IixkQXtmeOJ8CWj+995Pj47w==", + "dev": true, + "requires": { + "@babel/core": "^7.0.1", + "@babel/preset-env": "^7.0.0", + "babel-loader": "^8.0.2", + "bluebird": "3.5.0", + "debug": "3.1.0", + "lodash.clonedeep": "4.5.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "@cypress/xvfb": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.3.tgz", + "integrity": "sha512-yYrK+/bgL3hwoRHMZG4r5fyLniCy1pXex5fimtewAY6vE/jsVs8Q37UsEO03tFlcmiLnQ3rBNMaZBYTi/+C1cw==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, "@iarna/toml": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.1.tgz", @@ -933,6 +1082,34 @@ "integrity": "sha512-wYxU3kp5zItbxKmeRYCEplS2MW7DzyBnxPGj+GJVHZEUZiK/nn5Ei1sUFgURDh+X051+zsGe28iud3oHjrYWQQ==", "dev": true }, + "@types/blob-util": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz", + "integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==", + "dev": true + }, + "@types/bluebird": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.18.tgz", + "integrity": "sha512-OTPWHmsyW18BhrnG5x8F7PzeZ2nFxmHGb42bZn79P9hl+GI5cMzyPgQTwNjbem0lJhoru/8vtjAFCUOu3+gE2w==", + "dev": true + }, + "@types/chai": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.8.tgz", + "integrity": "sha512-m812CONwdZn/dMzkIJEY0yAs4apyTkTORgfB2UsMOxgkUbC205AHnm4T8I0I5gPg9MHrFc1dJ35iS75c0CJkjg==", + "dev": true + }, + "@types/chai-jquery": { + "version": "1.1.35", + "resolved": "https://registry.npmjs.org/@types/chai-jquery/-/chai-jquery-1.1.35.tgz", + "integrity": "sha512-7aIt9QMRdxuagLLI48dPz96YJdhu64p6FCa6n4qkGN5DQLHnrIjZpD9bXCvV2G0NwgZ1FAmfP214dxc5zNCfgQ==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/jquery": "*" + } + }, "@types/cheerio": { "version": "0.22.9", "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.9.tgz", @@ -1030,6 +1207,12 @@ "integrity": "sha512-G6EBrbjWDfmIpYu8UcRBOhwtDiYaLj5N5jUR5rx0YvbKxRBhXPZVLUmtfShewSUNKiQwpHavpML69a2WMbIlEQ==", "dev": true }, + "@types/jquery": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.6.tgz", + "integrity": "sha512-403D4wN95Mtzt2EoQHARf5oe/jEPhzBOBNrunk+ydQGW8WmkQ/E8rViRAEB1qEt/vssfGfNVD6ujP4FVeegrLg==", + "dev": true + }, "@types/level-codec": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@types/level-codec/-/level-codec-9.0.0.tgz", @@ -1053,6 +1236,18 @@ "integrity": "sha512-lRnAtKnxMXcYYXqOiotTmJd74uawNWuPnsnPrrO7HiFuE3npE2iQhfABatbYDyxTNqZNuXzcKGhw37R7RjBFLg==", "dev": true }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/mocha": { + "version": "2.2.44", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.44.tgz", + "integrity": "sha512-k2tWTQU8G4+iSMvqKi0Q9IIsWAp/n8xzdZS4Q4YVIltApoMA00wFBFdlJnmoaK1/z7B0Cy0yPe6GgXteSmdUNw==", + "dev": true + }, "@types/node": { "version": "9.6.34", "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.34.tgz", @@ -1115,9 +1310,9 @@ } }, "@types/react-dom": { - "version": "16.8.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.1.tgz", - "integrity": "sha512-Vyo4LqUvpjNC9RMXV6kXcsvW6U/WKOuHbz+mtY43Fu8AslHjQ5/Yx+sj0agGLkbnqOlQgyIgosewcxdjMirVXA==", + "version": "16.8.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.2.tgz", + "integrity": "sha512-MX7n1wq3G/De15RGAAqnmidzhr2Y9O/ClxPxyqaNg96pGyeXUYPSvujgzEVpLo9oIP4Wn1UETl+rxTN02KEpBw==", "dev": true, "requires": { "@types/react": "*" @@ -1192,6 +1387,22 @@ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", "dev": true }, + "@types/sinon": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.0.tgz", + "integrity": "sha512-kcYoPw0uKioFVC/oOqafk2yizSceIQXCYnkYts9vJIwQklFRsMubTObTDrjQamUyBRd47332s85074cd/hCwxg==", + "dev": true + }, + "@types/sinon-chai": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", + "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, "@types/text-encoding": { "version": "0.0.32", "resolved": "https://registry.npmjs.org/@types/text-encoding/-/text-encoding-0.0.32.tgz", @@ -1207,6 +1418,190 @@ "@types/node": "*" } }, + "@webassemblyjs/ast": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz", + "integrity": "sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/wast-parser": "1.7.11" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz", + "integrity": "sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz", + "integrity": "sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz", + "integrity": "sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz", + "integrity": "sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.7.11" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz", + "integrity": "sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz", + "integrity": "sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg==", + "dev": true + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz", + "integrity": "sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz", + "integrity": "sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-buffer": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/wasm-gen": "1.7.11" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz", + "integrity": "sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.7.11.tgz", + "integrity": "sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.1" + } + }, + "@webassemblyjs/utf8": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.7.11.tgz", + "integrity": "sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz", + "integrity": "sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-buffer": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/helper-wasm-section": "1.7.11", + "@webassemblyjs/wasm-gen": "1.7.11", + "@webassemblyjs/wasm-opt": "1.7.11", + "@webassemblyjs/wasm-parser": "1.7.11", + "@webassemblyjs/wast-printer": "1.7.11" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz", + "integrity": "sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/ieee754": "1.7.11", + "@webassemblyjs/leb128": "1.7.11", + "@webassemblyjs/utf8": "1.7.11" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz", + "integrity": "sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-buffer": "1.7.11", + "@webassemblyjs/wasm-gen": "1.7.11", + "@webassemblyjs/wasm-parser": "1.7.11" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz", + "integrity": "sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-api-error": "1.7.11", + "@webassemblyjs/helper-wasm-bytecode": "1.7.11", + "@webassemblyjs/ieee754": "1.7.11", + "@webassemblyjs/leb128": "1.7.11", + "@webassemblyjs/utf8": "1.7.11" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz", + "integrity": "sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/floating-point-hex-parser": "1.7.11", + "@webassemblyjs/helper-api-error": "1.7.11", + "@webassemblyjs/helper-code-frame": "1.7.11", + "@webassemblyjs/helper-fsm": "1.7.11", + "@xtuc/long": "4.2.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.7.11", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz", + "integrity": "sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/wast-parser": "1.7.11", + "@xtuc/long": "4.2.1" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.1.tgz", + "integrity": "sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==", + "dev": true + }, "abab": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", @@ -1238,9 +1633,15 @@ } }, "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz", + "integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", "dev": true }, "acorn-globals": { @@ -1250,6 +1651,14 @@ "dev": true, "requires": { "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true + } } }, "acorn-walk": { @@ -1264,17 +1673,29 @@ "integrity": "sha1-0ME1RB+oAUqBN5BFMQlvZ/KPJjo=" }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", + "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", "dev": true, "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, "alphanum-sort": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", @@ -1528,6 +1949,12 @@ "default-require-extensions": "^2.0.0" } }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1885,6 +2312,18 @@ } } }, + "babel-loader": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.5.tgz", + "integrity": "sha512-NTnHnVRd2JnRqPC0vW+iOQWU5pchDbYXsG2E6DMXEpMfUcQKclF9gmf3G3ZMhzG7IG9ji4coL0cm+FxeWxDpnw==", + "dev": true, + "requires": { + "find-cache-dir": "^2.0.0", + "loader-utils": "^1.0.2", + "mkdirp": "^0.5.1", + "util.promisify": "^1.0.0" + } + }, "babel-plugin-istanbul": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.1.1.tgz", @@ -1894,6 +2333,51 @@ "find-up": "^3.0.0", "istanbul-lib-instrument": "^3.0.0", "test-exclude": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "dev": true + } } }, "babel-plugin-jest-hoist": { @@ -2022,6 +2506,12 @@ "tweetnacl": "^0.14.3" } }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, "bignumber.js": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.1.0.tgz", @@ -2039,6 +2529,12 @@ "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=", "dev": true }, + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=", + "dev": true + }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", @@ -2160,6 +2656,12 @@ } } }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "browserify-aes": { "version": "1.2.0", "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -2287,6 +2789,12 @@ "isarray": "^1.0.0" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-equal": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", @@ -2333,6 +2841,57 @@ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", "dev": true }, + "cacache": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz", + "integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==", + "dev": true, + "requires": { + "bluebird": "^3.5.3", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "bluebird": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", + "dev": true + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + } + } + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -2350,6 +2909,15 @@ "unset-value": "^1.0.0" } }, + "cachedir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-1.3.0.tgz", + "integrity": "sha512-O1ji32oyON9laVPJL1IZ5bmwd2cB46VfpxkDequezH+15FDzzVddEyrGEeX4WusDSqKxdyFdDQDEG1yo1GoWkg==", + "dev": true, + "requires": { + "os-homedir": "^1.0.1" + } + }, "calculate-size": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/calculate-size/-/calculate-size-1.1.1.tgz", @@ -2386,9 +2954,9 @@ "dev": true }, "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, "caniuse-api": { @@ -2456,6 +3024,18 @@ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz", "integrity": "sha512-7I/xceXfKyUJmSAn/jw8ve/9DyOP7XxufNYLI9Px7CmsKgEUaZLUTax6nZxGQtaoiZCjpu6cHPj20xC/vqRReQ==" }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true + }, + "check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=", + "dev": true + }, "cheerio": { "version": "1.0.0-rc.2", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", @@ -2541,15 +3121,30 @@ } } }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", + "dev": true + }, "chroma-js": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-1.4.0.tgz", "integrity": "sha512-5vBYGJkhSnK2SRZ0XkxwTL+TSRyP7PHIxjeg+1uce5qpNDRLLwAXcF12kIztas/BYakWPQhchzV4TKkiwKNd8Q==" }, + "chrome-trace-event": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz", + "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", "dev": true }, "cipher-base": { @@ -2639,6 +3234,45 @@ "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==", "dev": true }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "dev": true, + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + } + } + }, + "cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "dev": true, + "requires": { + "slice-ansi": "0.0.4", + "string-width": "^1.0.1" + } + }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -2648,6 +3282,24 @@ "string-width": "^2.1.1", "strip-ansi": "^4.0.0", "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + } } }, "clone": { @@ -2814,6 +3466,21 @@ "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", "dev": true }, + "common-tags": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.4.0.tgz", + "integrity": "sha1-EYe+Tz1M8MBCfUP3Tu8fc1AWFMA=", + "dev": true, + "requires": { + "babel-runtime": "^6.18.0" + } + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, "compare-versions": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.4.0.tgz", @@ -2917,6 +3584,20 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", "dev": true }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -3036,6 +3717,12 @@ "custom-event": "1.0.0" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -3278,6 +3965,221 @@ "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.0.tgz", "integrity": "sha1-LkYovhncSyFLXAJjDFlx6BFhgGI=" }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "cypress": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-3.1.5.tgz", + "integrity": "sha512-jzYGKJqU1CHoNocPndinf/vbG28SeU+hg+4qhousT/HDBMJxYgjecXOmSgBX/ga9/TakhqSrIrSP2r6gW/OLtg==", + "dev": true, + "requires": { + "@cypress/listr-verbose-renderer": "0.4.1", + "@cypress/xvfb": "1.2.3", + "@types/blob-util": "1.3.3", + "@types/bluebird": "3.5.18", + "@types/chai": "4.0.8", + "@types/chai-jquery": "1.1.35", + "@types/jquery": "3.3.6", + "@types/lodash": "4.14.87", + "@types/minimatch": "3.0.3", + "@types/mocha": "2.2.44", + "@types/sinon": "7.0.0", + "@types/sinon-chai": "3.2.2", + "bluebird": "3.5.0", + "cachedir": "1.3.0", + "chalk": "2.4.1", + "check-more-types": "2.24.0", + "commander": "2.11.0", + "common-tags": "1.4.0", + "debug": "3.1.0", + "execa": "0.10.0", + "executable": "4.1.1", + "extract-zip": "1.6.6", + "fs-extra": "4.0.1", + "getos": "3.1.0", + "glob": "7.1.2", + "is-ci": "1.0.10", + "is-installed-globally": "0.1.0", + "lazy-ass": "1.6.0", + "listr": "0.12.0", + "lodash": "4.17.11", + "log-symbols": "2.2.0", + "minimist": "1.2.0", + "moment": "2.22.2", + "ramda": "0.24.1", + "request": "2.87.0", + "request-progress": "0.3.1", + "supports-color": "5.1.0", + "tmp": "0.0.31", + "url": "0.11.0", + "yauzl": "2.8.0" + }, + "dependencies": { + "@types/lodash": { + "version": "4.14.87", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.87.tgz", + "integrity": "sha512-AqRC+aEF4N0LuNHtcjKtvF9OTfqZI0iaBoe3dA6m/W+/YZJBZjBmW/QIZ8fBeXC6cnytSY9tBoFBqZ9uSCeVsw==", + "dev": true + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "dev": true, + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + } + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "is-ci": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.0.10.tgz", + "integrity": "sha1-9zkzayYyNlBhqdSCcM1WrjNpMY4=", + "dev": true, + "requires": { + "ci-info": "^1.0.0" + } + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "ramda": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.24.1.tgz", + "integrity": "sha1-w7d1UZfzW43DUCIoJixMkd22uFc=", + "dev": true + }, + "request": { + "version": "2.87.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", + "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "supports-color": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", + "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "requires": { + "punycode": "^1.4.1" + } + } + } + }, "d3-array": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", @@ -3365,6 +4267,12 @@ } } }, + "date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", + "dev": true + }, "date-now": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", @@ -3728,6 +4636,18 @@ "readable-stream": "^2.0.2" } }, + "duplexify": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", + "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, "dygraphs": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/dygraphs/-/dygraphs-2.1.0.tgz", @@ -3790,6 +4710,12 @@ "integrity": "sha512-nLo03Qpw++8R6BxDZL/B1c8SQvUe/htdgc5LWYHe5YotV2jVvRUMP5AlOmxOsyeOzgMiXrNln2mC05Ixz6vuUQ==", "dev": true }, + "elegant-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", + "dev": true + }, "element-resize-event": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/element-resize-event/-/element-resize-event-2.0.9.tgz", @@ -3810,6 +4736,12 @@ "minimalistic-crypto-utils": "^1.0.0" } }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -3845,6 +4777,17 @@ "once": "^1.4.0" } }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, "entities": { "version": "1.1.1", "resolved": "http://registry.npmjs.org/entities/-/entities-1.1.1.tgz", @@ -4004,12 +4947,31 @@ } } }, + "eslint-scope": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", + "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, "esprima": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", "dev": true }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, "estraverse": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", @@ -4060,18 +5022,40 @@ } }, "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "requires": { + "pify": "^2.2.0" } }, "exit": { @@ -4080,6 +5064,12 @@ "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", "dev": true }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -4201,6 +5191,55 @@ } } }, + "extract-zip": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "dev": true, + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "~1.0.1" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -4234,9 +5273,9 @@ } }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, "fast-diff": { @@ -4519,6 +5558,31 @@ } } }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, "fileset": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", @@ -4562,13 +5626,78 @@ "unpipe": "~1.0.0" } }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "find-cache-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.0.0.tgz", + "integrity": "sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "commondir": "^1.0.1", + "make-dir": "^1.0.0", + "pkg-dir": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" } }, "flatten": { @@ -4577,6 +5706,16 @@ "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", "dev": true }, + "flush-write-stream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", + "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.4" + } + }, "follow-redirects": { "version": "1.5.8", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.8.tgz", @@ -4656,6 +5795,39 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-extra": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", + "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4684,8 +5856,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -4709,15 +5880,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4734,22 +5903,19 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -4880,8 +6046,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4895,7 +6060,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4912,7 +6076,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4921,15 +6084,13 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4950,7 +6111,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5039,8 +6199,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5054,7 +6213,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5150,8 +6308,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5193,7 +6350,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5215,7 +6371,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5264,15 +6419,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true, - "optional": true + "dev": true } } }, @@ -5306,13 +6459,10 @@ "dev": true }, "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } + "version": "3.0.0", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true }, "get-value": { "version": "2.0.6", @@ -5320,6 +6470,26 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, + "getos": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.1.0.tgz", + "integrity": "sha512-i9vrxtDu5DlLVFcrbqUqGWYlZN/zZ4pGMICCAcZoYsX3JA54nYp8r5EThw5K+m2q3wszkx4Th746JstspB0H4Q==", + "dev": true, + "requires": { + "async": "2.4.0" + }, + "dependencies": { + "async": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.4.0.tgz", + "integrity": "sha1-SZAgDxjqW4N8LMT4wDGmmFw4VhE=", + "dev": true, + "requires": { + "lodash": "^4.14.0" + } + } + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -5376,6 +6546,15 @@ "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", "dev": true }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, "globals": { "version": "11.9.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", @@ -5398,6 +6577,12 @@ "unicode-trie": "^0.3.1" } }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -5438,6 +6623,32 @@ "requires": { "ajv": "^5.3.0", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + } } }, "harmony-reflect": { @@ -5528,6 +6739,12 @@ "minimalistic-assert": "^1.0.1" } }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, "hex-color-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", @@ -6366,6 +7583,12 @@ "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", "dev": true }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, "immediate": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", @@ -6402,6 +7625,15 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, "indexes-of": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", @@ -6444,9 +7676,9 @@ } }, "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "dev": true }, "ipaddr.js": { @@ -6531,6 +7763,14 @@ "dev": true, "requires": { "ci-info": "^2.0.0" + }, + "dependencies": { + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + } } }, "is-color-stop": { @@ -6606,11 +7846,23 @@ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } }, "is-generator-fn": { "version": "2.0.0", @@ -6623,6 +7875,16 @@ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz", "integrity": "sha512-but/G3sapV3MNyqiDBLrOi4x8uCIw0RY3o/Vb5GT0sMFHrVV7731wFSVy41T5FO1og7G0gXLJh0MkgPRouko/A==" }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -6644,6 +7906,15 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", @@ -6658,6 +7929,12 @@ "isobject": "^3.0.1" } }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -6997,6 +8274,32 @@ "requires": { "execa": "^1.0.0", "throat": "^4.0.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } } }, "jest-config": { @@ -7620,6 +8923,12 @@ "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", "dev": true }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true + }, "pretty-format": { "version": "24.0.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.0.0.tgz", @@ -7673,9 +8982,9 @@ } }, "js-levenshtein": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz", - "integrity": "sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", "dev": true }, "js-tokens": { @@ -7740,6 +9049,12 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", "dev": true + }, + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "dev": true } } }, @@ -7762,9 +9077,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "json-stringify-safe": { @@ -7782,6 +9097,15 @@ "minimist": "^1.2.0" } }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -7809,13 +9133,19 @@ "integrity": "sha512-3h7B2WRT5LNXOtQiAaWonilegHcPSf9nLVXlSTci8lu1dZUuui61+EsPEZqSVxY7rXYmB2DVKMQILxaO5WL61Q==", "dev": true }, + "lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", + "dev": true + }, "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", "dev": true, "requires": { - "invert-kv": "^2.0.0" + "invert-kv": "^1.0.0" } }, "left-pad": { @@ -7886,6 +9216,264 @@ "type-check": "~0.3.2" } }, + "listr": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.12.0.tgz", + "integrity": "sha1-a84sD1YD+klYDqF81qAMwOX6RRo=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "figures": "^1.7.0", + "indent-string": "^2.1.0", + "is-promise": "^2.1.0", + "is-stream": "^1.1.0", + "listr-silent-renderer": "^1.1.1", + "listr-update-renderer": "^0.2.0", + "listr-verbose-renderer": "^0.4.0", + "log-symbols": "^1.0.2", + "log-update": "^1.0.2", + "ora": "^0.2.3", + "p-map": "^1.1.1", + "rxjs": "^5.0.0-beta.11", + "stream-to-observable": "^0.1.0", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-spinners": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz", + "integrity": "sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw=", + "dev": true + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "requires": { + "chalk": "^1.0.0" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "ora": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz", + "integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=", + "dev": true, + "requires": { + "chalk": "^1.1.1", + "cli-cursor": "^1.0.2", + "cli-spinners": "^0.1.2", + "object-assign": "^4.0.1" + } + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "listr-silent-renderer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", + "dev": true + }, + "listr-update-renderer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz", + "integrity": "sha1-yoDhd5tOcCZoB+ju0a1qvjmFUPk=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "elegant-spinner": "^1.0.1", + "figures": "^1.7.0", + "indent-string": "^3.0.0", + "log-symbols": "^1.0.2", + "log-update": "^1.0.2", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "requires": { + "chalk": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "listr-verbose-renderer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", + "integrity": "sha1-ggb0z21S3cWCfl/RSYng6WWTOjU=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cli-cursor": "^1.0.2", + "date-fns": "^1.27.2", + "figures": "^1.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -7896,15 +9484,51 @@ "parse-json": "^4.0.0", "pify": "^3.0.0", "strip-bom": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { - "p-locate": "^3.0.0", + "p-locate": "^2.0.0", "path-exists": "^3.0.0" } }, @@ -7929,6 +9553,12 @@ "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=", "dev": true }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -7962,6 +9592,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -7988,6 +9624,49 @@ "chalk": "^2.0.1" } }, + "log-update": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz", + "integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=", + "dev": true, + "requires": { + "ansi-escapes": "^1.0.0", + "cli-cursor": "^1.0.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + } + } + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8035,6 +9714,14 @@ "dev": true, "requires": { "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, "make-error": { @@ -8092,6 +9779,17 @@ "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", "dev": true }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "dev": true, + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -8124,14 +9822,12 @@ "dev": true }, "mem": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", - "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", "dev": true, "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^2.0.0" + "mimic-fn": "^1.0.0" } }, "memoize-one": { @@ -8147,6 +9843,16 @@ "map-or-similar": "^1.5.0" } }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, "merge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", @@ -8311,6 +10017,24 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, "mixin-deep": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", @@ -8349,6 +10073,78 @@ } } }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "mocha-junit-reporter": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-1.18.0.tgz", + "integrity": "sha512-y3XuqKa2+HRYtg0wYyhW/XsLm2Ps+pqf9HaTAt7+MVUAKFJaNAHOrNseTZo9KCxjfIbxUWwckP5qCDDPUmjSWA==", + "dev": true, + "requires": { + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "strip-ansi": "^4.0.0", + "xml": "^1.0.0" + } + }, "moment": { "version": "2.22.2", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", @@ -8360,6 +10156,20 @@ "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==", "dev": true }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -8463,6 +10273,12 @@ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", "dev": true }, + "neo-async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", + "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", + "dev": true + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -8821,7 +10637,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, @@ -8874,14 +10690,14 @@ "dev": true }, "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", "dev": true, "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" } }, "os-tmpdir": { @@ -8928,23 +10744,29 @@ "dev": true }, "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "requires": { - "p-try": "^2.0.0" + "p-try": "^1.0.0" } }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-limit": "^1.1.0" } }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, "p-reduce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", @@ -8952,9 +10774,9 @@ "dev": true }, "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, "pako": { @@ -8968,6 +10790,17 @@ "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-4.6.1.tgz", "integrity": "sha512-X9Ws5tnEQKRCZRfoojX3KvRZbLY1BbL0wqSHF3CKGmxD8Zr4E0WaipUuFweffkCN8RSQzHKhb/F+ATYdNcz1rg==" }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, "parcel": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/parcel/-/parcel-1.11.0.tgz", @@ -9357,6 +11190,12 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -9382,6 +11221,14 @@ "dev": true, "requires": { "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, "pbkdf2": { @@ -9397,6 +11244,12 @@ "sha.js": "^2.4.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -9409,9 +11262,9 @@ "dev": true }, "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, "pirates": { @@ -9430,6 +11283,51 @@ "dev": true, "requires": { "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "dev": true + } } }, "pn": { @@ -10313,6 +12211,12 @@ "asap": "~2.0.3" } }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, "prompts": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.0.3.tgz", @@ -10389,6 +12293,29 @@ "once": "^1.3.1" } }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -10762,6 +12689,51 @@ "requires": { "find-up": "^3.0.0", "read-pkg": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "dev": true + } } }, "readable-stream": { @@ -11120,6 +13092,64 @@ "resolved": "https://registry.npmjs.org/regexp-quote/-/regexp-quote-0.0.0.tgz", "integrity": "sha1-Hg9GUMhi3L/tVP1CsUjpuxch/PI=" }, + "regexp-tree": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.0.tgz", + "integrity": "sha512-rHQv+tzu+0l3KS/ERabas1yK49ahNVxuH40WcPg53CzP5p8TgmmyBgHELLyJcvjhTD0e5ahSY6C76LbEVtr7cg==", + "dev": true, + "requires": { + "cli-table3": "^0.5.0", + "colors": "^1.1.2", + "yargs": "^10.0.3" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "yargs": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz", + "integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^8.1.0" + } + }, + "yargs-parser": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", + "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, "regexpu-core": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.4.0.tgz", @@ -11196,6 +13226,15 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, "replace-ext": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", @@ -11229,6 +13268,15 @@ "uuid": "^3.3.2" } }, + "request-progress": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-0.3.1.tgz", + "integrity": "sha1-ByHBBdipasayzossia4tXs/Pazo=", + "dev": true, + "requires": { + "throttleit": "~0.0.2" + } + }, "request-promise-core": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", @@ -11382,6 +13430,32 @@ "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", "dev": true }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + }, + "dependencies": { + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "dev": true + } + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -11426,6 +13500,32 @@ "minimist": "^1.1.1", "walker": "~1.0.5", "watch": "~0.18.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } } }, "sanitize-html-react": { @@ -11471,6 +13571,42 @@ "object-assign": "^4.1.1" } }, + "schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", + "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, "seleccion": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/seleccion/-/seleccion-2.0.0.tgz", @@ -11508,6 +13644,12 @@ "statuses": "~1.4.0" } }, + "serialize-javascript": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.6.1.tgz", + "integrity": "sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==", + "dev": true + }, "serialize-to-js": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-1.2.2.tgz", @@ -11637,6 +13779,12 @@ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", "dev": true }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -11693,6 +13841,12 @@ "is-plain-obj": "^1.0.0" } }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -11821,6 +13975,15 @@ "tweetnacl": "~0.14.0" } }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -11839,9 +14002,9 @@ "integrity": "sha512-Qe8QntFrrpWTnHwvwj2FZTgv+PKIsp0B9VxLzLLbSpPXWOgRgc5LVj/aTiSfK1RqIeF9jeC1UeOH8Q8y60A7og==" }, "static-eval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.1.tgz", - "integrity": "sha512-1JJ8ADJ7UB//CRqocI6j4WxGvSqQHX14Fz0gXDNvRA6Y1JIAI/lMNdqn1lpnaA6ugQ0fMH0uBB955DkwhKActw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.0.tgz", + "integrity": "sha512-6flshd3F1Gwm+Ksxq463LtFd1liC77N/PX1FVVc3OzL3hAmo2fwHFbuArkcfi7s9rTNsLEhcRmXGFZhlgy40uw==", "dev": true, "requires": { "escodegen": "^1.8.1" @@ -11929,6 +14092,16 @@ "readable-stream": "^2.0.2" } }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, "stream-http": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", @@ -11942,6 +14115,18 @@ "xtend": "^4.0.0" } }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "stream-to-observable": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.1.0.tgz", + "integrity": "sha1-Rb8dny19wJvtgfHDB8Qw5ouEz/4=", + "dev": true + }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -11958,13 +14143,25 @@ } }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } } }, "string.prototype.trim": { @@ -12081,6 +14278,12 @@ "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", "dev": true }, + "tapable": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", + "integrity": "sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==", + "dev": true + }, "terser": { "version": "3.14.0", "resolved": "https://registry.npmjs.org/terser/-/terser-3.14.0.tgz", @@ -12106,6 +14309,65 @@ } } }, + "terser-webpack-plugin": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.1.tgz", + "integrity": "sha512-GGSt+gbT0oKcMDmPx4SRSfJPE1XaN3kQRWG4ghxKQw9cn5G9x6aCKSsgYdvyM0na9NJ4Drv0RG6jbBByZ5CMjw==", + "dev": true, + "requires": { + "cacache": "^11.0.2", + "find-cache-dir": "^2.0.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^1.4.0", + "source-map": "^0.6.1", + "terser": "^3.8.1", + "webpack-sources": "^1.1.0", + "worker-farm": "^1.5.2" + }, + "dependencies": { + "ajv": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", + "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "test-exclude": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.1.0.tgz", @@ -12124,6 +14386,12 @@ "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", "dev": true }, + "throttleit": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", + "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", + "dev": true + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -12160,6 +14428,15 @@ "integrity": "sha1-k9nez/yIBb1X6uQxDwt0Xptvs6c=", "dev": true }, + "tmp": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.1" + } + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -12329,6 +14606,228 @@ "resolve": "1.x", "semver": "^5.5", "yargs-parser": "10.x" + }, + "dependencies": { + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + } + } + }, + "ts-loader": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.3.3.tgz", + "integrity": "sha512-KwF1SplmOJepnoZ4eRIloH/zXL195F51skt7reEsS6jvDqzgc/YSbz9b8E07GxIUwLXdcD4ssrJu6v8CwaTafA==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^3.1.4", + "semver": "^5.0.1" + }, + "dependencies": { + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + } + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } } }, "tslib": { @@ -12588,6 +15087,24 @@ "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", "dev": true }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz", + "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, "unist-util-is": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.2.tgz", @@ -12629,6 +15146,12 @@ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz", "integrity": "sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q==" }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -12687,6 +15210,23 @@ "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", "dev": true }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -12872,6 +15412,17 @@ "minimist": "^1.2.0" } }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "dev": true, + "requires": { + "chokidar": "^2.0.2", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -12887,6 +15438,284 @@ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", "dev": true }, + "webpack": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.29.0.tgz", + "integrity": "sha512-pxdGG0keDBtamE1mNvT5zyBdx+7wkh6mh7uzMOo/uRQ/fhsdj5FXkh/j5mapzs060forql1oXqXN9HJGju+y7w==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.7.11", + "@webassemblyjs/helper-module-context": "1.7.11", + "@webassemblyjs/wasm-edit": "1.7.11", + "@webassemblyjs/wasm-parser": "1.7.11", + "acorn": "^6.0.5", + "acorn-dynamic-import": "^4.0.0", + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", + "chrome-trace-event": "^1.0.0", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.0", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.3.0", + "loader-utils": "^1.1.0", + "memory-fs": "~0.4.1", + "micromatch": "^3.1.8", + "mkdirp": "~0.5.0", + "neo-async": "^2.5.0", + "node-libs-browser": "^2.0.0", + "schema-utils": "^0.4.4", + "tapable": "^1.1.0", + "terser-webpack-plugin": "^1.1.0", + "watchpack": "^1.5.0", + "webpack-sources": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.5.tgz", + "integrity": "sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg==", + "dev": true + }, + "ajv": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", + "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + } + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "webpack-sources": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "whatwg-encoding": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", @@ -12958,6 +15787,15 @@ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, + "worker-farm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", + "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, "wrap-ansi": { "version": "2.1.0", "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -12968,26 +15806,6 @@ "strip-ansi": "^3.0.1" }, "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -13030,6 +15848,12 @@ "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=" }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, "xml-name-validator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", @@ -13042,9 +15866,15 @@ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true }, "yargs": { @@ -13067,34 +15897,155 @@ "yargs-parser": "^11.1.1" }, "dependencies": { - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "mem": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", + "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^1.0.0", + "p-is-promise": "^2.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" } } } }, "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", "dev": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" }, "dependencies": { "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", "dev": true } } + }, + "yauzl": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.8.0.tgz", + "integrity": "sha1-eUUK/yKyqcWkHvVOAtuQfM+/nuI=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.0.1" + } } } } diff --git a/ui/package.json b/ui/package.json index 847f739728..f18476937b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -23,11 +23,14 @@ "test:watch": "jest --watch --verbose false", "test:update": "jest --updateSnapshot", "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --verbose false", - "lint": "npm run tslint && npm run tsc", - "tslint": "tslint -c ./tslint.json '{src,test}/**/*.ts?(x)'", - "tslint:fix": "tslint --fix -c ./tslint.json '{src,test}/**/*.ts?(x)'", - "tsc": "tsc -p ./tsconfig.json --noEmit --pretty", - "tsc:watch": "tsc -p ./tsconfig.json --noEmit --pretty -w" + "test:junit": "cypress run --reporter junit --reporter-options 'mochaFile=junit-results/my-test-output.xml'", + "lint": "npm run tslint && npm run tsc && npm run tsc:cypress", + "tslint": "tslint -c ./tslint.json '{src,cypress}/**/*.ts?(x)'", + "tslint:fix": "tslint --fix -c ./tslint.json '{src,cypress}/**/*.ts?(x)'", + "tsc": "tsc -p ./tsconfig.json --noEmit --pretty --skipLibCheck", + "tsc:watch": "tsc -p ./tsconfig.json --noEmit --pretty -w", + "tsc:cypress": "tsc -p ./cypress/tsconfig.json --noEmit --pretty --skipLibCheck", + "cypress:open": "cypress open" }, "jest": { "setupTestFrameworkScriptFile": "./jestSetup.ts", @@ -68,6 +71,9 @@ }, "author": "", "devDependencies": { + "@babel/core": "^7.2.2", + "@babel/preset-env": "^7.3.1", + "@cypress/webpack-preprocessor": "^4.0.3", "@types/chroma-js": "^1.3.4", "@types/codemirror": "^0.0.56", "@types/d3-color": "^1.2.1", @@ -94,7 +100,11 @@ "@types/react-virtualized": "^9.18.3", "@types/text-encoding": "^0.0.32", "@types/uuid": "^3.4.3", + "acorn": "^6.0.6", + "ajv": "^6.7.0", "autoprefixer": "^6.3.1", + "babel-loader": "^8.0.5", + "cypress": "^3.1.5", "enzyme": "^3.6.0", "enzyme-adapter-react-16": "^1.6.0", "enzyme-to-json": "^3.3.4", @@ -104,17 +114,21 @@ "jest": "^24.1.0", "jest-runner-tslint": "^1.0.4", "jsdom": "^9.0.0", + "mocha": "^5.2.0", + "mocha-junit-reporter": "^1.18.0", "parcel": "^1.11.0", "prettier": "^1.14.3", "react-testing-library": "^5.4.4", "sass": "^1.15.3", "ts-jest": "^24.0.0", + "ts-loader": "^5.3.3", "tslib": "^1.9.0", "tslint": "^5.9.1", "tslint-config-prettier": "^1.15.0", "tslint-plugin-prettier": "^2.0.0", "tslint-react": "^3.5.1", - "typescript": "^3.1.3" + "typescript": "^3.1.3", + "webpack": "^4.29.0" }, "dependencies": { "@influxdata/clockface": "0.0.4", diff --git a/ui/src/clockface/components/index_views/IndexListRow.tsx b/ui/src/clockface/components/index_views/IndexListRow.tsx index dfbfdc2cee..f9e1436b24 100644 --- a/ui/src/clockface/components/index_views/IndexListRow.tsx +++ b/ui/src/clockface/components/index_views/IndexListRow.tsx @@ -9,18 +9,24 @@ interface Props { disabled?: boolean children: JSX.Element[] | JSX.Element customClass?: string + testID: string } @ErrorHandling class IndexListRow extends Component { public static defaultProps: Partial = { disabled: false, + testID: 'table-row', } public render() { - const {children} = this.props + const {children, testID} = this.props - return {children} + return ( + + {children} + + ) } private get className(): string { diff --git a/ui/src/clockface/components/index_views/test/__snapshots__/IndexList.test.tsx.snap b/ui/src/clockface/components/index_views/test/__snapshots__/IndexList.test.tsx.snap index b20f57b088..a669606d22 100644 --- a/ui/src/clockface/components/index_views/test/__snapshots__/IndexList.test.tsx.snap +++ b/ui/src/clockface/components/index_views/test/__snapshots__/IndexList.test.tsx.snap @@ -146,9 +146,11 @@ exports[`IndexList matches snapshot with minimal props 1`] = ` > { const {children} = this.props return ( -
+
{children}
) diff --git a/ui/src/clockface/components/radio_buttons/RadioButton.tsx b/ui/src/clockface/components/radio_buttons/RadioButton.tsx index 5a984002f4..1ba057158b 100644 --- a/ui/src/clockface/components/radio_buttons/RadioButton.tsx +++ b/ui/src/clockface/components/radio_buttons/RadioButton.tsx @@ -14,6 +14,7 @@ interface Props { disabled?: boolean titleText: string disabledTitleText?: string + testID?: string } @ErrorHandling @@ -21,10 +22,11 @@ class RadioButton extends Component { public static defaultProps: Partial = { disabled: false, disabledTitleText: 'This option is disabled', + testID: 'radio-button', } public render() { - const {children, disabled} = this.props + const {children, disabled, testID} = this.props return ( diff --git a/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx b/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx index 770e229930..9a5bdc7fd3 100644 --- a/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx +++ b/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx @@ -129,8 +129,8 @@ class DashboardIndex extends PureComponent {
{ const {id} = dashboard return ( - + Dashboard Cell @@ -49,6 +50,7 @@ exports[`SaveAsButton rendering renders 1`] = ` disabled={false} disabledTitleText="This option is disabled" onClick={[Function]} + testID="radio-button" value="task" > Task diff --git a/ui/src/index.tsx b/ui/src/index.tsx index ea6e5b1174..c32fcda208 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -14,8 +14,6 @@ import {getBasepath} from 'src/utils/basepath' // Components import App from 'src/App' -import GetSources from 'src/shared/containers/GetSources' -import SetActiveSource from 'src/shared/containers/SetActiveSource' import GetOrganizations from 'src/shared/containers/GetOrganizations' import Setup from 'src/Setup' import Signin from 'src/Signin' @@ -39,8 +37,6 @@ import GetLinks from 'src/shared/containers/GetLinks' import GetMe from 'src/shared/containers/GetMe' import SourcesPage from 'src/sources/components/SourcesPage' import ConfigurationPage from 'src/configuration/components/ConfigurationPage' -import OrgDashboardsIndex from 'src/organizations/containers/OrgDashboardsIndex' -import OrgMembersIndex from 'src/organizations/containers/OrgMembersIndex' import OnboardingWizardPage from 'src/onboarding/containers/OnboardingWizardPage' @@ -103,64 +99,49 @@ class Root extends PureComponent { - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/ui/src/me/components/account/__snapshots__/Tokens.test.tsx.snap b/ui/src/me/components/account/__snapshots__/Tokens.test.tsx.snap index 25d26f8b68..cf37f5b5d2 100644 --- a/ui/src/me/components/account/__snapshots__/Tokens.test.tsx.snap +++ b/ui/src/me/components/account/__snapshots__/Tokens.test.tsx.snap @@ -287,9 +287,11 @@ exports[`Account rendering renders! 1`] = ` >
{ { { active={type === BucketRetentionRules.TypeEnum.Expire} onClick={this.handleRadioClick} value={BucketRetentionRules.TypeEnum.Expire} + testID="retention-intervals" > Periodically diff --git a/ui/src/organizations/components/__snapshots__/Buckets.test.tsx.snap b/ui/src/organizations/components/__snapshots__/Buckets.test.tsx.snap index 751ab30ed8..42d1efa383 100644 --- a/ui/src/organizations/components/__snapshots__/Buckets.test.tsx.snap +++ b/ui/src/organizations/components/__snapshots__/Buckets.test.tsx.snap @@ -37,6 +37,7 @@ Object { > { const {children} = this.props return ( -