Merge branch 'master' into multiple-event-handlers
commit
abdfd14bbd
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 1.4.0.0-beta1
|
||||
current_version = 1.4.0.0
|
||||
files = README.md server/swagger.json
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
|
||||
serialize = {major}.{minor}.{patch}.{release}
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
npm-debug.log
|
||||
*_gen.go
|
||||
canned/apps_gen.go
|
||||
server/swagger_gen.go
|
||||
.pull-request
|
||||
node_modules
|
||||
/chronograf
|
||||
node_modules/
|
||||
# Directories
|
||||
ui/reports/
|
||||
build/
|
||||
chronograf.db
|
||||
chronograf-v1.db
|
||||
npm-debug.log
|
||||
.vscode
|
||||
backup/
|
||||
.vscode/
|
||||
|
||||
# Binaries
|
||||
/chronograf
|
||||
|
||||
# Dotfiles
|
||||
.pull-request
|
||||
.DS_Store
|
||||
.godep
|
||||
.jsdep
|
||||
.jssrc
|
||||
.dev-jssrc
|
||||
.bindata
|
||||
ui/reports
|
||||
|
||||
# Files
|
||||
chronograf*.db
|
||||
*_gen.go
|
||||
canned/apps_gen.go
|
||||
npm-debug.log
|
||||
|
|
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -1,7 +1,41 @@
|
|||
## v1.4.0.0-beta2 [unreleased]
|
||||
### Features
|
||||
## v1.4.0.0 [2017-12-22]
|
||||
### UI Improvements
|
||||
1. [#2652](https://github.com/influxdata/chronograf/pull/2652): Add page header with instructional copy when adding initial source for consistency and clearer UX
|
||||
|
||||
### Bug Fixes
|
||||
1. [#2652](https://github.com/influxdata/chronograf/pull/2652): Make page render successfully when attempting to edit a source
|
||||
|
||||
## v1.4.0.0-rc2 [2017-12-21]
|
||||
### UI Improvements
|
||||
1. [#2632](https://github.com/influxdata/chronograf/pull/2632): Tell user which organization they switched into and what role they have whenever they switch, including on Source Page
|
||||
|
||||
### Bug Fixes
|
||||
1. [#2639](https://github.com/influxdata/chronograf/pull/2639): Prevent SuperAdmin from modifying their own status
|
||||
1. [#2632](https://github.com/influxdata/chronograf/pull/2632): Give SuperAdmin DefaultRole when switching to organization where they have no role
|
||||
1. [#2642](https://github.com/influxdata/chronograf/pull/2642): Fix DE query config on first run
|
||||
|
||||
## v1.4.0.0-rc1 [2017-12-19]
|
||||
### Features
|
||||
1. [#2593](https://github.com/influxdata/chronograf/pull/2593): Add option to use files for dashboards, organizations, data sources, and kapacitors
|
||||
1. [#2604](https://github.com/influxdata/chronograf/pull/2604): After chronograf version upgrade, backup database is created in ./backups
|
||||
|
||||
### UI Improvements
|
||||
1. [#2492](https://github.com/influxdata/chronograf/pull/2492): Cleanup style on login page with multiple OAuth2 providers
|
||||
|
||||
### Bug Fixes
|
||||
1. [#2502](https://github.com/influxdata/chronograf/pull/2502): Fix stale source data after updating or creating
|
||||
1. [#2616](https://github.com/influxdata/chronograf/pull/2616): Fix cell editing so query data choices are kept when updating a cell
|
||||
1. [#2612](https://github.com/influxdata/chronograf/pull/2612): Allow days as a valid duration value
|
||||
|
||||
## v1.4.0.0-beta2 [2017-12-14]
|
||||
### UI Improvements
|
||||
1. [#2502](https://github.com/influxdata/chronograf/pull/2502): Fix cursor flashing between default and pointer
|
||||
1. [#2598](https://github.com/influxdata/chronograf/pull/2598): Allow appendage of a suffix to single stat visualizations
|
||||
1. [#2598](https://github.com/influxdata/chronograf/pull/2598): Allow optional colorization of text instead of background on single stat visualizations
|
||||
|
||||
### Bug Fixes
|
||||
1. [#2528](https://github.com/influxdata/chronograf/pull/2528): Fix template rendering to ignore template if not in query
|
||||
1. [#2563](https://github.com/influxdata/chronograf/pull/2563): Fix graph inversion if user input y-axis min greater than max
|
||||
|
||||
## v1.4.0.0-beta1 [2017-12-07]
|
||||
### Features
|
||||
|
|
4
Makefile
4
Makefile
|
@ -93,10 +93,10 @@ internal.pb.go: bolt/internal/internal.proto
|
|||
test: jstest gotest gotestrace
|
||||
|
||||
gotest:
|
||||
go test `go list ./... | grep -v /vendor/`
|
||||
go test ./...
|
||||
|
||||
gotestrace:
|
||||
go test -race `go list ./... | grep -v /vendor/`
|
||||
go test -race ./...
|
||||
|
||||
jstest:
|
||||
cd ui && yarn test
|
||||
|
|
|
@ -136,7 +136,7 @@ option.
|
|||
## Versions
|
||||
|
||||
The most recent version of Chronograf is
|
||||
[v1.4.0.0-beta1](https://www.influxdata.com/downloads/).
|
||||
[v1.4.0.0](https://www.influxdata.com/downloads/).
|
||||
|
||||
Spotted a bug or have a feature request? Please open
|
||||
[an issue](https://github.com/influxdata/chronograf/issues/new)!
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package bolt_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/bolt"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
)
|
||||
|
||||
// TestNow is a set time for testing.
|
||||
|
@ -31,6 +34,13 @@ func NewTestClient() (*TestClient, error) {
|
|||
c.Path = f.Name()
|
||||
c.Now = func() time.Time { return TestNow }
|
||||
|
||||
build := chronograf.BuildInfo{
|
||||
Version: "version",
|
||||
Commit: "commit",
|
||||
}
|
||||
|
||||
c.Open(context.TODO(), mocks.NewLogger(), build)
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/bolt/internal"
|
||||
)
|
||||
|
||||
// Ensure BuildStore struct implements chronograf.BuildStore interface
|
||||
var _ chronograf.BuildStore = &BuildStore{}
|
||||
|
||||
// BuildBucket is the bolt bucket used to store Chronograf build information
|
||||
var BuildBucket = []byte("Build")
|
||||
|
||||
// BuildKey is the constant key used in the bolt bucket
|
||||
var BuildKey = []byte("build")
|
||||
|
||||
// BuildStore is a bolt implementation to store Chronograf build information
|
||||
type BuildStore struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Get retrieves Chronograf build information from the database
|
||||
func (s *BuildStore) Get(ctx context.Context) (chronograf.BuildInfo, error) {
|
||||
var build chronograf.BuildInfo
|
||||
if err := s.client.db.View(func(tx *bolt.Tx) error {
|
||||
var err error
|
||||
build, err = s.get(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return chronograf.BuildInfo{}, err
|
||||
}
|
||||
|
||||
return build, nil
|
||||
}
|
||||
|
||||
// Update overwrites the current Chronograf build information in the database
|
||||
func (s *BuildStore) Update(ctx context.Context, build chronograf.BuildInfo) error {
|
||||
if err := s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
return s.update(ctx, build, tx)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate simply stores the current version in the database
|
||||
func (s *BuildStore) Migrate(ctx context.Context, build chronograf.BuildInfo) error {
|
||||
return s.Update(ctx, build)
|
||||
}
|
||||
|
||||
// get retrieves the current build, falling back to a default when missing
|
||||
func (s *BuildStore) get(ctx context.Context, tx *bolt.Tx) (chronograf.BuildInfo, error) {
|
||||
var build chronograf.BuildInfo
|
||||
defaultBuild := chronograf.BuildInfo{
|
||||
Version: "pre-1.4.0.0",
|
||||
Commit: "",
|
||||
}
|
||||
|
||||
if bucket := tx.Bucket(BuildBucket); bucket == nil {
|
||||
return defaultBuild, nil
|
||||
} else if v := bucket.Get(BuildKey); v == nil {
|
||||
return defaultBuild, nil
|
||||
} else if err := internal.UnmarshalBuild(v, &build); err != nil {
|
||||
return build, err
|
||||
}
|
||||
return build, nil
|
||||
}
|
||||
|
||||
func (s *BuildStore) update(ctx context.Context, build chronograf.BuildInfo, tx *bolt.Tx) error {
|
||||
if v, err := internal.MarshalBuild(build); err != nil {
|
||||
return err
|
||||
} else if err := tx.Bucket(BuildBucket).Put(BuildKey, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package bolt_test
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
|
||||
// "github.com/google/go-cmp/cmp"
|
||||
// "github.com/influxdata/chronograf"
|
||||
// )
|
||||
|
||||
// func
|
||||
// func TestBuildStore_Get(t *testing.T) {
|
||||
// type wants struct {
|
||||
// build *chronograf.BuildInfo
|
||||
// err error
|
||||
// }
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// wants wants
|
||||
// }{
|
||||
// {
|
||||
// name: "When the build info is missing",
|
||||
// wants: wants{
|
||||
// build: &chronograf.BuildInfo{
|
||||
// Version: "pre-1.4.0.0",
|
||||
// Commit: "",
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// for _, tt := range tests {
|
||||
// client, err := NewTestClient()
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// if err := client.Open(context.TODO()); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// defer client.Close()
|
||||
|
||||
// b := client.BuildStore
|
||||
// got, err := b.Get(context.Background())
|
||||
// if (tt.wants.err != nil) != (err != nil) {
|
||||
// t.Errorf("%q. BuildStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
|
||||
// continue
|
||||
// }
|
||||
// if diff := cmp.Diff(got, tt.wants.build); diff != "" {
|
||||
// t.Errorf("%q. BuildStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestBuildStore_Update(t *testing.T) {
|
||||
|
||||
// }
|
190
bolt/client.go
190
bolt/client.go
|
@ -2,56 +2,122 @@ package bolt
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/uuid"
|
||||
"github.com/influxdata/chronograf/id"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrUnableToOpen means we had an issue establishing a connection (or creating the database)
|
||||
ErrUnableToOpen = "Unable to open boltdb; is there a chronograf already running? %v"
|
||||
// ErrUnableToBackup means we couldn't copy the db file into ./backup
|
||||
ErrUnableToBackup = "Unable to backup your database prior to migrations: %v"
|
||||
// ErrUnableToInitialize means we couldn't create missing Buckets (maybe a timeout)
|
||||
ErrUnableToInitialize = "Unable to boot boltdb: %v"
|
||||
// ErrUnableToMigrate means we had an issue changing the db schema
|
||||
ErrUnableToMigrate = "Unable to migrate boltdb: %v"
|
||||
)
|
||||
|
||||
// Client is a client for the boltDB data store.
|
||||
type Client struct {
|
||||
Path string
|
||||
db *bolt.DB
|
||||
logger chronograf.Logger
|
||||
isNew bool
|
||||
Now func() time.Time
|
||||
LayoutIDs chronograf.ID
|
||||
|
||||
BuildStore *BuildStore
|
||||
SourcesStore *SourcesStore
|
||||
ServersStore *ServersStore
|
||||
LayoutsStore *LayoutsStore
|
||||
DashboardsStore *DashboardsStore
|
||||
UsersStore *UsersStore
|
||||
OrganizationsStore *OrganizationsStore
|
||||
ConfigStore *ConfigStore
|
||||
}
|
||||
|
||||
// NewClient initializes all stores
|
||||
func NewClient() *Client {
|
||||
c := &Client{Now: time.Now}
|
||||
c.BuildStore = &BuildStore{client: c}
|
||||
c.SourcesStore = &SourcesStore{client: c}
|
||||
c.ServersStore = &ServersStore{client: c}
|
||||
c.LayoutsStore = &LayoutsStore{
|
||||
client: c,
|
||||
IDs: &uuid.V4{},
|
||||
IDs: &id.UUID{},
|
||||
}
|
||||
c.DashboardsStore = &DashboardsStore{
|
||||
client: c,
|
||||
IDs: &uuid.V4{},
|
||||
IDs: &id.UUID{},
|
||||
}
|
||||
c.UsersStore = &UsersStore{client: c}
|
||||
c.OrganizationsStore = &OrganizationsStore{client: c}
|
||||
c.ConfigStore = &ConfigStore{client: c}
|
||||
return c
|
||||
}
|
||||
|
||||
// Open and initialize boltDB. Initial buckets are created if they do not exist.
|
||||
func (c *Client) Open(ctx context.Context) error {
|
||||
// Option to change behavior of Open()
|
||||
type Option interface {
|
||||
Backup() bool
|
||||
}
|
||||
|
||||
// WithBackup returns a Backup
|
||||
func WithBackup() Option {
|
||||
return Backup{}
|
||||
}
|
||||
|
||||
// Backup tells Open to perform a backup prior to initialization
|
||||
type Backup struct {
|
||||
}
|
||||
|
||||
// Backup returns true
|
||||
func (b Backup) Backup() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Open / create boltDB file.
|
||||
func (c *Client) Open(ctx context.Context, logger chronograf.Logger, build chronograf.BuildInfo, opts ...Option) error {
|
||||
if _, err := os.Stat(c.Path); os.IsNotExist(err) {
|
||||
c.isNew = true
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open database file.
|
||||
db, err := bolt.Open(c.Path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf(ErrUnableToOpen, err)
|
||||
}
|
||||
c.db = db
|
||||
c.logger = logger
|
||||
|
||||
for _, opt := range opts {
|
||||
if opt.Backup() {
|
||||
if err = c.backup(ctx, build); err != nil {
|
||||
return fmt.Errorf(ErrUnableToBackup, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = c.initialize(ctx); err != nil {
|
||||
return fmt.Errorf(ErrUnableToInitialize, err)
|
||||
}
|
||||
if err = c.migrate(ctx, build); err != nil {
|
||||
return fmt.Errorf(ErrUnableToMigrate, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initialize creates Buckets that are missing
|
||||
func (c *Client) initialize(ctx context.Context) error {
|
||||
if err := c.db.Update(func(tx *bolt.Tx) error {
|
||||
// Always create Organizations bucket.
|
||||
if _, err := tx.CreateBucketIfNotExists(OrganizationsBucket); err != nil {
|
||||
|
@ -77,28 +143,48 @@ func (c *Client) Open(ctx context.Context) error {
|
|||
if _, err := tx.CreateBucketIfNotExists(UsersBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
// Always create Config bucket.
|
||||
if _, err := tx.CreateBucketIfNotExists(ConfigBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
// Always create Build bucket.
|
||||
if _, err := tx.CreateBucketIfNotExists(BuildBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Runtime migrations
|
||||
if err := c.OrganizationsStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.SourcesStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.ServersStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.LayoutsStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.DashboardsStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrate moves data from an old schema to a new schema in each Store
|
||||
func (c *Client) migrate(ctx context.Context, build chronograf.BuildInfo) error {
|
||||
if c.db != nil {
|
||||
// Runtime migrations
|
||||
if err := c.OrganizationsStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.SourcesStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.ServersStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.LayoutsStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.DashboardsStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.ConfigStore.Migrate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.BuildStore.Migrate(ctx, build); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -110,6 +196,66 @@ func (c *Client) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// copy creates a copy of the database in toFile
|
||||
func (c *Client) copy(ctx context.Context, version string) error {
|
||||
backupDir := path.Join(path.Dir(c.Path), "backup")
|
||||
if _, err := os.Stat(backupDir); os.IsNotExist(err) {
|
||||
if err = os.Mkdir(backupDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fromFile, err := os.Open(c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fromFile.Close()
|
||||
|
||||
toName := fmt.Sprintf("%s.%s", path.Base(c.Path), version)
|
||||
toPath := path.Join(backupDir, toName)
|
||||
toFile, err := os.OpenFile(toPath, os.O_RDWR|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer toFile.Close()
|
||||
|
||||
_, err = io.Copy(toFile, fromFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.logger.Info("Successfully created ", toPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// backup makes a copy of the database to the backup/ directory, if necessary:
|
||||
// - If this is a fresh install, don't create a backup and store the current version
|
||||
// - If we are on the same version, don't create a backup
|
||||
// - If the version has changed, create a backup and store the current version
|
||||
func (c *Client) backup(ctx context.Context, build chronograf.BuildInfo) error {
|
||||
lastBuild, err := c.BuildStore.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lastBuild.Version == build.Version {
|
||||
return nil
|
||||
}
|
||||
if c.isNew {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The database was pre-existing, and the version has changed
|
||||
// and so create a backup
|
||||
|
||||
c.logger.Info("Moving from version ", lastBuild.Version)
|
||||
c.logger.Info("Moving to version ", build.Version)
|
||||
|
||||
return c.copy(ctx, lastBuild.Version)
|
||||
}
|
||||
|
||||
func bucket(b []byte, org string) []byte {
|
||||
return []byte(path.Join(string(b), org))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/bolt/internal"
|
||||
)
|
||||
|
||||
// Ensure ConfigStore implements chronograf.ConfigStore.
|
||||
var _ chronograf.ConfigStore = &ConfigStore{}
|
||||
|
||||
// ConfigBucket is used to store chronograf application state
|
||||
var ConfigBucket = []byte("ConfigV1")
|
||||
|
||||
// configID is the boltDB key where the configuration object is stored
|
||||
var configID = []byte("config/v1")
|
||||
|
||||
// ConfigStore uses bolt to store and retrieve global
|
||||
// application configuration
|
||||
type ConfigStore struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (s *ConfigStore) Migrate(ctx context.Context) error {
|
||||
if _, err := s.Get(ctx); err != nil {
|
||||
return s.Initialize(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ConfigStore) Initialize(ctx context.Context) error {
|
||||
cfg := chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
}
|
||||
return s.Update(ctx, &cfg)
|
||||
}
|
||||
|
||||
func (s *ConfigStore) Get(ctx context.Context) (*chronograf.Config, error) {
|
||||
var cfg chronograf.Config
|
||||
err := s.client.db.View(func(tx *bolt.Tx) error {
|
||||
v := tx.Bucket(ConfigBucket).Get(configID)
|
||||
if v == nil {
|
||||
return chronograf.ErrConfigNotFound
|
||||
}
|
||||
return internal.UnmarshalConfig(v, &cfg)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (s *ConfigStore) Update(ctx context.Context, cfg *chronograf.Config) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config provided was nil")
|
||||
}
|
||||
return s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
if v, err := internal.MarshalConfig(cfg); err != nil {
|
||||
return err
|
||||
} else if err := tx.Bucket(ConfigBucket).Put(configID, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package bolt_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
func TestConfig_Get(t *testing.T) {
|
||||
type wants struct {
|
||||
config *chronograf.Config
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "Get config",
|
||||
wants: wants{
|
||||
config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
client, err := NewTestClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.ConfigStore
|
||||
got, err := s.Get(context.Background())
|
||||
if (tt.wants.err != nil) != (err != nil) {
|
||||
t.Errorf("%q. ConfigStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
|
||||
continue
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.wants.config); diff != "" {
|
||||
t.Errorf("%q. ConfigStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_Update(t *testing.T) {
|
||||
type args struct {
|
||||
config *chronograf.Config
|
||||
}
|
||||
type wants struct {
|
||||
config *chronograf.Config
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "Set config",
|
||||
args: args{
|
||||
config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
client, err := NewTestClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.ConfigStore
|
||||
err = s.Update(context.Background(), tt.args.config)
|
||||
if (tt.wants.err != nil) != (err != nil) {
|
||||
t.Errorf("%q. ConfigStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
|
||||
continue
|
||||
}
|
||||
|
||||
got, _ := s.Get(context.Background())
|
||||
if (tt.wants.err != nil) != (err != nil) {
|
||||
t.Errorf("%q. ConfigStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err)
|
||||
continue
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(got, tt.wants.config); diff != "" {
|
||||
t.Errorf("%q. ConfigStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package bolt
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -64,11 +63,9 @@ func (d *DashboardsStore) Migrate(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
|
||||
|
||||
for _, board := range boards {
|
||||
if board.Organization == "" {
|
||||
board.Organization = defaultOrgID
|
||||
board.Organization = defaultOrg.ID
|
||||
if err := d.Update(ctx, board); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package internal
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/influxdata/chronograf"
|
||||
|
@ -9,6 +10,26 @@ import (
|
|||
|
||||
//go:generate protoc --gogo_out=. internal.proto
|
||||
|
||||
// MarshalBuild encodes a build to binary protobuf format.
|
||||
func MarshalBuild(b chronograf.BuildInfo) ([]byte, error) {
|
||||
return proto.Marshal(&BuildInfo{
|
||||
Version: b.Version,
|
||||
Commit: b.Commit,
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalBuild decodes a build from binary protobuf data.
|
||||
func UnmarshalBuild(data []byte, b *chronograf.BuildInfo) error {
|
||||
var pb BuildInfo
|
||||
if err := proto.Unmarshal(data, &pb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Version = pb.Version
|
||||
b.Commit = pb.Commit
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalSource encodes a source to binary protobuf format.
|
||||
func MarshalSource(s chronograf.Source) ([]byte, error) {
|
||||
return proto.Marshal(&Source{
|
||||
|
@ -591,3 +612,39 @@ func UnmarshalOrganizationPB(data []byte, o *Organization) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalConfig encodes a config to binary protobuf format.
|
||||
func MarshalConfig(c *chronograf.Config) ([]byte, error) {
|
||||
return MarshalConfigPB(&Config{
|
||||
Auth: &AuthConfig{
|
||||
SuperAdminNewUsers: c.Auth.SuperAdminNewUsers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalConfigPB encodes a config to binary protobuf format.
|
||||
func MarshalConfigPB(c *Config) ([]byte, error) {
|
||||
return proto.Marshal(c)
|
||||
}
|
||||
|
||||
// UnmarshalConfig decodes a config from binary protobuf data.
|
||||
func UnmarshalConfig(data []byte, c *chronograf.Config) error {
|
||||
var pb Config
|
||||
if err := UnmarshalConfigPB(data, &pb); err != nil {
|
||||
return err
|
||||
}
|
||||
if pb.Auth == nil {
|
||||
return fmt.Errorf("Auth config is nil")
|
||||
}
|
||||
c.Auth.SuperAdminNewUsers = pb.Auth.SuperAdminNewUsers
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalConfigPB decodes a config from binary protobuf data.
|
||||
func UnmarshalConfigPB(data []byte, c *Config) error {
|
||||
if err := proto.Unmarshal(data, c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -158,12 +158,25 @@ message Role {
|
|||
}
|
||||
|
||||
message Organization {
|
||||
uint64 ID = 1; // ID is the unique ID of the organization
|
||||
string ID = 1; // ID is the unique ID of the organization
|
||||
string Name = 2; // Name is the organization's name
|
||||
string DefaultRole = 3; // DefaultRole is the name of the role that is the default for any users added to the organization
|
||||
bool Public = 4; // Public specifies that users must be explicitly added to the organization
|
||||
}
|
||||
|
||||
message Config {
|
||||
AuthConfig Auth = 1; // Auth is the configuration for options that auth related
|
||||
}
|
||||
|
||||
message AuthConfig {
|
||||
bool SuperAdminNewUsers = 1; // SuperAdminNewUsers configuration option that specifies which users will auto become super admin
|
||||
}
|
||||
|
||||
message BuildInfo {
|
||||
string Version = 1; // Version is a descriptive git SHA identifier
|
||||
string Commit = 2; // Commit is an abbreviated SHA
|
||||
}
|
||||
|
||||
// The following is a vim modeline, it autoconfigures vim to have the
|
||||
// appropriate tabbing and whitespace management to edit this file
|
||||
//
|
||||
|
|
|
@ -13,12 +13,14 @@ import (
|
|||
// Ensure OrganizationsStore implements chronograf.OrganizationsStore.
|
||||
var _ chronograf.OrganizationsStore = &OrganizationsStore{}
|
||||
|
||||
// OrganizationsBucket is the bucket where organizations are stored.
|
||||
var OrganizationsBucket = []byte("OrganizationsV1")
|
||||
var (
|
||||
// OrganizationsBucket is the bucket where organizations are stored.
|
||||
OrganizationsBucket = []byte("OrganizationsV1")
|
||||
// DefaultOrganizationID is the ID of the default organization.
|
||||
DefaultOrganizationID = []byte("default")
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultOrganizationID is the ID of the default organization.
|
||||
DefaultOrganizationID uint64 = 0
|
||||
// DefaultOrganizationName is the Name of the default organization
|
||||
DefaultOrganizationName string = "Default"
|
||||
// DefaultOrganizationRole is the DefaultRole for the Default organization
|
||||
|
@ -40,20 +42,20 @@ func (s *OrganizationsStore) Migrate(ctx context.Context) error {
|
|||
// CreateDefault does a findOrCreate on the default organization
|
||||
func (s *OrganizationsStore) CreateDefault(ctx context.Context) error {
|
||||
o := chronograf.Organization{
|
||||
ID: DefaultOrganizationID,
|
||||
ID: string(DefaultOrganizationID),
|
||||
Name: DefaultOrganizationName,
|
||||
DefaultRole: DefaultOrganizationRole,
|
||||
Public: DefaultOrganizationPublic,
|
||||
}
|
||||
return s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(OrganizationsBucket)
|
||||
v := b.Get(u64tob(o.ID))
|
||||
v := b.Get(DefaultOrganizationID)
|
||||
if v != nil {
|
||||
return nil
|
||||
}
|
||||
if v, err := internal.MarshalOrganization(&o); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(u64tob(o.ID), v); err != nil {
|
||||
} else if err := b.Put(DefaultOrganizationID, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -75,7 +77,7 @@ func (s *OrganizationsStore) nameIsUnique(ctx context.Context, name string) bool
|
|||
func (s *OrganizationsStore) DefaultOrganization(ctx context.Context) (*chronograf.Organization, error) {
|
||||
var org chronograf.Organization
|
||||
if err := s.client.db.View(func(tx *bolt.Tx) error {
|
||||
v := tx.Bucket(OrganizationsBucket).Get(u64tob(DefaultOrganizationID))
|
||||
v := tx.Bucket(OrganizationsBucket).Get(DefaultOrganizationID)
|
||||
return internal.UnmarshalOrganization(v, &org)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
@ -89,25 +91,23 @@ func (s *OrganizationsStore) Add(ctx context.Context, o *chronograf.Organization
|
|||
if !s.nameIsUnique(ctx, o.Name) {
|
||||
return nil, chronograf.ErrOrganizationAlreadyExists
|
||||
}
|
||||
if err := s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
err := s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(OrganizationsBucket)
|
||||
seq, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.ID = seq
|
||||
if v, err := internal.MarshalOrganization(o); err != nil {
|
||||
return err
|
||||
} else if err := b.Put(u64tob(seq), v); err != nil {
|
||||
o.ID = fmt.Sprintf("%d", seq)
|
||||
|
||||
v, err := internal.MarshalOrganization(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Put([]byte(o.ID), v)
|
||||
})
|
||||
|
||||
return o, nil
|
||||
return o, err
|
||||
}
|
||||
|
||||
// All returns all known organizations
|
||||
|
@ -126,7 +126,7 @@ func (s *OrganizationsStore) All(ctx context.Context) ([]chronograf.Organization
|
|||
|
||||
// Delete the organization from OrganizationsStore
|
||||
func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organization) error {
|
||||
if o.ID == DefaultOrganizationID {
|
||||
if o.ID == string(DefaultOrganizationID) {
|
||||
return chronograf.ErrCannotDeleteDefaultOrganization
|
||||
}
|
||||
_, err := s.get(ctx, o.ID)
|
||||
|
@ -134,19 +134,18 @@ func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organizat
|
|||
return err
|
||||
}
|
||||
if err := s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(OrganizationsBucket).Delete(u64tob(o.ID))
|
||||
return tx.Bucket(OrganizationsBucket).Delete([]byte(o.ID))
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Dependent Delete of all resources
|
||||
|
||||
org := fmt.Sprintf("%d", o.ID)
|
||||
// Each of the associated organization stores expects organization to be
|
||||
// set on the context.
|
||||
ctx = context.WithValue(ctx, organizations.ContextKey, org)
|
||||
ctx = context.WithValue(ctx, organizations.ContextKey, o.ID)
|
||||
|
||||
sourcesStore := organizations.NewSourcesStore(s.client.SourcesStore, org)
|
||||
sourcesStore := organizations.NewSourcesStore(s.client.SourcesStore, o.ID)
|
||||
sources, err := sourcesStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -157,7 +156,7 @@ func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organizat
|
|||
}
|
||||
}
|
||||
|
||||
serversStore := organizations.NewServersStore(s.client.ServersStore, org)
|
||||
serversStore := organizations.NewServersStore(s.client.ServersStore, o.ID)
|
||||
servers, err := serversStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -168,7 +167,7 @@ func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organizat
|
|||
}
|
||||
}
|
||||
|
||||
dashboardsStore := organizations.NewDashboardsStore(s.client.DashboardsStore, org)
|
||||
dashboardsStore := organizations.NewDashboardsStore(s.client.DashboardsStore, o.ID)
|
||||
dashboards, err := dashboardsStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -179,7 +178,7 @@ func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organizat
|
|||
}
|
||||
}
|
||||
|
||||
usersStore := organizations.NewUsersStore(s.client.UsersStore, org)
|
||||
usersStore := organizations.NewUsersStore(s.client.UsersStore, o.ID)
|
||||
users, err := usersStore.All(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -193,10 +192,10 @@ func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organizat
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *OrganizationsStore) get(ctx context.Context, id uint64) (*chronograf.Organization, error) {
|
||||
func (s *OrganizationsStore) get(ctx context.Context, id string) (*chronograf.Organization, error) {
|
||||
var o chronograf.Organization
|
||||
err := s.client.db.View(func(tx *bolt.Tx) error {
|
||||
v := tx.Bucket(OrganizationsBucket).Get(u64tob(id))
|
||||
v := tx.Bucket(OrganizationsBucket).Get([]byte(id))
|
||||
if v == nil {
|
||||
return chronograf.ErrOrganizationNotFound
|
||||
}
|
||||
|
@ -221,7 +220,6 @@ func (s *OrganizationsStore) each(ctx context.Context, fn func(*chronograf.Organ
|
|||
return nil
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns a Organization if the id exists.
|
||||
|
@ -270,7 +268,7 @@ func (s *OrganizationsStore) Update(ctx context.Context, o *chronograf.Organizat
|
|||
return s.client.db.Update(func(tx *bolt.Tx) error {
|
||||
if v, err := internal.MarshalOrganization(o); err != nil {
|
||||
return err
|
||||
} else if err := tx.Bucket(OrganizationsBucket).Put(u64tob(o.ID), v); err != nil {
|
||||
} else if err := tx.Bucket(OrganizationsBucket).Put([]byte(o.ID), v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -57,10 +57,6 @@ func TestOrganizationsStore_GetWithName(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.OrganizationsStore
|
||||
|
@ -103,7 +99,7 @@ func TestOrganizationsStore_GetWithID(t *testing.T) {
|
|||
args: args{
|
||||
ctx: context.Background(),
|
||||
org: &chronograf.Organization{
|
||||
ID: 1234,
|
||||
ID: "1234",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
|
@ -129,10 +125,6 @@ func TestOrganizationsStore_GetWithID(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.OrganizationsStore
|
||||
|
@ -188,11 +180,6 @@ func TestOrganizationsStore_All(t *testing.T) {
|
|||
},
|
||||
},
|
||||
want: []chronograf.Organization{
|
||||
{
|
||||
Name: bolt.DefaultOrganizationName,
|
||||
DefaultRole: bolt.DefaultOrganizationRole,
|
||||
Public: bolt.DefaultOrganizationPublic,
|
||||
},
|
||||
{
|
||||
Name: "EE - Evil Empire",
|
||||
DefaultRole: roles.MemberRoleName,
|
||||
|
@ -203,6 +190,11 @@ func TestOrganizationsStore_All(t *testing.T) {
|
|||
DefaultRole: roles.EditorRoleName,
|
||||
Public: true,
|
||||
},
|
||||
{
|
||||
Name: bolt.DefaultOrganizationName,
|
||||
DefaultRole: bolt.DefaultOrganizationRole,
|
||||
Public: bolt.DefaultOrganizationPublic,
|
||||
},
|
||||
},
|
||||
addFirst: true,
|
||||
},
|
||||
|
@ -214,10 +206,6 @@ func TestOrganizationsStore_All(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.OrganizationsStore
|
||||
|
@ -265,7 +253,7 @@ func TestOrganizationsStore_Update(t *testing.T) {
|
|||
args: args{
|
||||
ctx: context.Background(),
|
||||
initial: &chronograf.Organization{
|
||||
ID: 1234,
|
||||
ID: "1234",
|
||||
Name: "The Okay Place",
|
||||
},
|
||||
updates: &chronograf.Organization{},
|
||||
|
@ -399,10 +387,8 @@ func TestOrganizationsStore_Update(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.OrganizationsStore
|
||||
|
||||
for _, org := range tt.fields.orgs {
|
||||
|
@ -462,7 +448,7 @@ func TestOrganizationStore_Delete(t *testing.T) {
|
|||
args: args{
|
||||
ctx: context.Background(),
|
||||
org: &chronograf.Organization{
|
||||
ID: 10,
|
||||
ID: "10",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
|
@ -483,10 +469,8 @@ func TestOrganizationStore_Delete(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.OrganizationsStore
|
||||
|
||||
if tt.addFirst {
|
||||
|
@ -520,10 +504,8 @@ func TestOrganizationStore_DeleteDefaultOrg(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.OrganizationsStore
|
||||
|
||||
defaultOrg, err := s.DefaultOrganization(tt.args.ctx)
|
||||
|
@ -574,10 +556,8 @@ func TestOrganizationsStore_Add(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.OrganizationsStore
|
||||
|
||||
for _, org := range tt.fields.orgs {
|
||||
|
@ -635,7 +615,7 @@ func TestOrganizationsStore_DefaultOrganization(t *testing.T) {
|
|||
ctx: context.Background(),
|
||||
},
|
||||
want: &chronograf.Organization{
|
||||
ID: bolt.DefaultOrganizationID,
|
||||
ID: string(bolt.DefaultOrganizationID),
|
||||
Name: bolt.DefaultOrganizationName,
|
||||
DefaultRole: bolt.DefaultOrganizationRole,
|
||||
Public: bolt.DefaultOrganizationPublic,
|
||||
|
@ -648,9 +628,6 @@ func TestOrganizationsStore_DefaultOrganization(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
s := client.OrganizationsStore
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package bolt
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/influxdata/chronograf"
|
||||
|
@ -32,11 +31,9 @@ func (s *ServersStore) Migrate(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
|
||||
|
||||
for _, server := range servers {
|
||||
if server.Organization == "" {
|
||||
server.Organization = defaultOrgID
|
||||
server.Organization = defaultOrg.ID
|
||||
if err := s.Update(ctx, server); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -14,10 +14,8 @@ func TestServerStore(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
s := c.ServersStore
|
||||
|
||||
srcs := []chronograf.Server{
|
||||
|
|
|
@ -2,7 +2,6 @@ package bolt
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/influxdata/chronograf"
|
||||
|
@ -32,11 +31,9 @@ func (s *SourcesStore) Migrate(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
|
||||
|
||||
for _, source := range sources {
|
||||
if source.Organization == "" {
|
||||
source.Organization = defaultOrgID
|
||||
source.Organization = defaultOrg.ID
|
||||
}
|
||||
if source.Role == "" {
|
||||
source.Role = roles.ViewerRoleName
|
||||
|
|
|
@ -15,10 +15,8 @@ func TestSourceStore(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
s := c.SourcesStore
|
||||
|
||||
srcs := []chronograf.Source{
|
||||
|
|
|
@ -2,7 +2,6 @@ package bolt_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
@ -62,9 +61,6 @@ func TestUsersStore_GetWithID(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.UsersStore
|
||||
|
@ -134,9 +130,6 @@ func TestUsersStore_GetWithNameProviderScheme(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.UsersStore
|
||||
|
@ -167,9 +160,6 @@ func TestUsersStore_GetInvalid(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.UsersStore
|
||||
|
@ -242,10 +232,8 @@ func TestUsersStore_Add(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.UsersStore
|
||||
if tt.args.addFirst {
|
||||
_, _ = s.Add(tt.args.ctx, tt.args.u)
|
||||
|
@ -307,16 +295,12 @@ func TestUsersStore_Delete(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.UsersStore
|
||||
|
||||
if tt.addFirst {
|
||||
var err error
|
||||
tt.args.user, err = s.Add(tt.args.ctx, tt.args.user)
|
||||
fmt.Println(err)
|
||||
tt.args.user, _ = s.Add(tt.args.ctx, tt.args.user)
|
||||
}
|
||||
if err := s.Delete(tt.args.ctx, tt.args.user); (err != nil) != tt.wantErr {
|
||||
t.Errorf("%q. UsersStore.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
|
@ -408,10 +392,8 @@ func TestUsersStore_Update(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.UsersStore
|
||||
|
||||
if tt.addFirst {
|
||||
|
@ -499,10 +481,8 @@ func TestUsersStore_All(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.UsersStore
|
||||
|
||||
if tt.addFirst {
|
||||
|
@ -567,10 +547,8 @@ func TestUsersStore_Num(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Open(context.TODO()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
s := client.UsersStore
|
||||
|
||||
for _, u := range tt.users {
|
||||
|
|
|
@ -16,16 +16,20 @@ const (
|
|||
ErrDashboardNotFound = Error("dashboard not found")
|
||||
ErrUserNotFound = Error("user not found")
|
||||
ErrLayoutInvalid = Error("layout is invalid")
|
||||
ErrDashboardInvalid = Error("dashboard is invalid")
|
||||
ErrSourceInvalid = Error("source is invalid")
|
||||
ErrServerInvalid = Error("server is invalid")
|
||||
ErrAlertNotFound = Error("alert not found")
|
||||
ErrAuthentication = Error("user not authenticated")
|
||||
ErrUninitialized = Error("client uninitialized. Call Open() method")
|
||||
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
|
||||
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold'")
|
||||
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold', 'text', and 'background'")
|
||||
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
|
||||
ErrUserAlreadyExists = Error("user already exists")
|
||||
ErrOrganizationNotFound = Error("organization not found")
|
||||
ErrOrganizationAlreadyExists = Error("organization already exists")
|
||||
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
|
||||
ErrConfigNotFound = Error("cannot find configuration")
|
||||
)
|
||||
|
||||
// Error is a domain error encountered while processing chronograf requests
|
||||
|
@ -561,7 +565,7 @@ type LayoutsStore interface {
|
|||
|
||||
// Organization is a group of resources under a common name
|
||||
type Organization struct {
|
||||
ID uint64 `json:"id,string"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
// DefaultRole is the name of the role that is the default for any users added to the organization
|
||||
DefaultRole string `json:"defaultRole,omitempty"`
|
||||
|
@ -575,7 +579,7 @@ type Organization struct {
|
|||
// It is expected that only one of ID or Name will be specified, but will prefer ID over Name if both are specified.
|
||||
type OrganizationQuery struct {
|
||||
// If an ID is provided in the query, the lookup time for an organization will be O(1).
|
||||
ID *uint64
|
||||
ID *string
|
||||
// If Name is provided, the lookup time will be O(n).
|
||||
Name *string
|
||||
}
|
||||
|
@ -604,3 +608,48 @@ type OrganizationsStore interface {
|
|||
// DefaultOrganization returns the DefaultOrganization
|
||||
DefaultOrganization(ctx context.Context) (*Organization, error)
|
||||
}
|
||||
|
||||
// AuthConfig is the global application config section for auth parameters
|
||||
|
||||
type AuthConfig struct {
|
||||
// SuperAdminNewUsers should be true by default to give a seamless upgrade to
|
||||
// 1.4.0 for legacy users. It means that all new users will by default receive
|
||||
// SuperAdmin status. If a SuperAdmin wants to change this behavior, they
|
||||
// can toggle it off via the Chronograf UI, in which case newly authenticating
|
||||
// users will simply receive whatever role they would otherwise receive.
|
||||
SuperAdminNewUsers bool `json:"superAdminNewUsers"`
|
||||
}
|
||||
|
||||
// Config is the global application Config for parameters that can be set via
|
||||
// API, with different sections, such as Auth
|
||||
type Config struct {
|
||||
Auth AuthConfig `json:"auth"`
|
||||
}
|
||||
|
||||
// ConfigStore is the storage and retrieval of global application Config
|
||||
type ConfigStore interface {
|
||||
// Initialize creates the initial configuration
|
||||
Initialize(context.Context) error
|
||||
// Get retrieves the whole Config from the ConfigStore
|
||||
Get(context.Context) (*Config, error)
|
||||
// Update updates the whole Config in the ConfigStore
|
||||
Update(context.Context, *Config) error
|
||||
}
|
||||
|
||||
// BuildInfo is sent to the usage client to track versions and commits
|
||||
type BuildInfo struct {
|
||||
Version string
|
||||
Commit string
|
||||
}
|
||||
|
||||
// BuildStore is the storage and retrieval of Chronograf build information
|
||||
type BuildStore interface {
|
||||
Get(context.Context) (BuildInfo, error)
|
||||
Update(context.Context, BuildInfo) error
|
||||
}
|
||||
|
||||
// Environement is the set of front-end exposed environment variables
|
||||
// that were set on the server
|
||||
type Environment struct {
|
||||
TelegrafSystemInterval time.Duration `json:"telegrafSystemInterval"`
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/server"
|
||||
flags "github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
@ -17,7 +18,7 @@ var (
|
|||
|
||||
func main() {
|
||||
srv := server.Server{
|
||||
BuildInfo: server.BuildInfo{
|
||||
BuildInfo: chronograf.BuildInfo{
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
},
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
download a binary here https://github.com/google/protobuf/releases/tag/v3.1.0
|
||||
Download the protobuf binary by either:
|
||||
- `brew install protobuf`
|
||||
- Download from protobuf [github release](https://github.com/google/protobuf/releases/tag/v3.1.0) and place in your $PATH
|
||||
|
||||
|
||||
run the following 4 commands listed here https://github.com/gogo/protobuf
|
||||
```sh
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package canned
|
||||
package filestore
|
||||
|
||||
import (
|
||||
"context"
|
|
@ -1,4 +1,4 @@
|
|||
package canned_test
|
||||
package filestore_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -13,7 +13,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/canned"
|
||||
"github.com/influxdata/chronograf/filestore"
|
||||
clog "github.com/influxdata/chronograf/log"
|
||||
)
|
||||
|
||||
|
@ -47,7 +47,7 @@ func TestAll(t *testing.T) {
|
|||
apps, _ := MockApps(test.Existing, test.Err)
|
||||
layouts, err := apps.All(context.Background())
|
||||
if err != test.Err {
|
||||
t.Errorf("Test %d: Canned all error expected: %v; actual: %v", i, test.Err, err)
|
||||
t.Errorf("Test %d: apps all error expected: %v; actual: %v", i, test.Err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(layouts, test.Existing) {
|
||||
t.Errorf("Test %d: Layouts should be equal; expected %v; actual %v", i, test.Existing, layouts)
|
||||
|
@ -99,7 +99,7 @@ func TestAdd(t *testing.T) {
|
|||
apps, _ := MockApps(test.Existing, test.Err)
|
||||
layout, err := apps.Add(context.Background(), test.Add)
|
||||
if err != test.Err {
|
||||
t.Errorf("Test %d: Canned add error expected: %v; actual: %v", i, test.Err, err)
|
||||
t.Errorf("Test %d: apps add error expected: %v; actual: %v", i, test.Err, err)
|
||||
}
|
||||
|
||||
if layout.ID != test.ExpectedID {
|
||||
|
@ -150,7 +150,7 @@ func TestDelete(t *testing.T) {
|
|||
apps, actual := MockApps(test.Existing, test.Err)
|
||||
err := apps.Delete(context.Background(), chronograf.Layout{ID: test.DeleteID})
|
||||
if err != test.Err {
|
||||
t.Errorf("Test %d: Canned delete error expected: %v; actual: %v", i, test.Err, err)
|
||||
t.Errorf("Test %d: apps delete error expected: %v; actual: %v", i, test.Err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(*actual, test.Expected) {
|
||||
t.Errorf("Test %d: Layouts should be equal; expected %v; actual %v", i, test.Expected, actual)
|
||||
|
@ -199,7 +199,7 @@ func TestGet(t *testing.T) {
|
|||
apps, _ := MockApps(test.Existing, test.Err)
|
||||
layout, err := apps.Get(context.Background(), test.ID)
|
||||
if err != test.Err {
|
||||
t.Errorf("Test %d: Canned get error expected: %v; actual: %v", i, test.Err, err)
|
||||
t.Errorf("Test %d: Layouts get error expected: %v; actual: %v", i, test.Err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(layout, test.Expected) {
|
||||
t.Errorf("Test %d: Layouts should be equal; expected %v; actual %v", i, test.Expected, layout)
|
||||
|
@ -261,7 +261,7 @@ func TestUpdate(t *testing.T) {
|
|||
apps, actual := MockApps(test.Existing, test.Err)
|
||||
err := apps.Update(context.Background(), test.Update)
|
||||
if err != test.Err {
|
||||
t.Errorf("Test %d: Canned get error expected: %v; actual: %v", i, test.Err, err)
|
||||
t.Errorf("Test %d: Layouts get error expected: %v; actual: %v", i, test.Err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(*actual, test.Expected) {
|
||||
t.Errorf("Test %d: Layouts should be equal; expected %v; actual %v", i, test.Expected, actual)
|
||||
|
@ -312,7 +312,7 @@ func (m *MockID) Generate() (string, error) {
|
|||
return strconv.Itoa(m.id), nil
|
||||
}
|
||||
|
||||
func MockApps(existing []chronograf.Layout, expected error) (canned.Apps, *map[string]chronograf.Layout) {
|
||||
func MockApps(existing []chronograf.Layout, expected error) (filestore.Apps, *map[string]chronograf.Layout) {
|
||||
layouts := map[string]chronograf.Layout{}
|
||||
fileName := func(dir string, layout chronograf.Layout) string {
|
||||
return path.Join(dir, layout.ID+".json")
|
||||
|
@ -326,11 +326,11 @@ func MockApps(existing []chronograf.Layout, expected error) (canned.Apps, *map[s
|
|||
return chronograf.Layout{}, expected
|
||||
}
|
||||
|
||||
if l, ok := layouts[file]; !ok {
|
||||
l, ok := layouts[file]
|
||||
if !ok {
|
||||
return chronograf.Layout{}, chronograf.ErrLayoutNotFound
|
||||
} else {
|
||||
return l, nil
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
create := func(file string, layout chronograf.Layout) error {
|
||||
|
@ -346,7 +346,7 @@ func MockApps(existing []chronograf.Layout, expected error) (canned.Apps, *map[s
|
|||
return nil, expected
|
||||
}
|
||||
info := []os.FileInfo{}
|
||||
for k, _ := range layouts {
|
||||
for k := range layouts {
|
||||
info = append(info, &MockFileInfo{filepath.Base(k)})
|
||||
}
|
||||
sort.Sort(MockFileInfos(info))
|
||||
|
@ -364,7 +364,7 @@ func MockApps(existing []chronograf.Layout, expected error) (canned.Apps, *map[s
|
|||
return nil
|
||||
}
|
||||
|
||||
return canned.Apps{
|
||||
return filestore.Apps{
|
||||
Dir: dir,
|
||||
Load: load,
|
||||
Filename: fileName,
|
|
@ -0,0 +1,210 @@
|
|||
package filestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// DashExt is the the file extension searched for in the directory for dashboard files
|
||||
const DashExt = ".dashboard"
|
||||
|
||||
var _ chronograf.DashboardsStore = &Dashboards{}
|
||||
|
||||
// Dashboards are JSON dashboards stored in the filesystem
|
||||
type Dashboards struct {
|
||||
Dir string // Dir is the directory containing the dashboards.
|
||||
Load func(string, interface{}) error // Load loads string name and dashbaord passed in as interface
|
||||
Create func(string, interface{}) error // Create will write dashboard to file.
|
||||
ReadDir func(dirname string) ([]os.FileInfo, error) // ReadDir reads the directory named by dirname and returns a list of directory entries sorted by filename.
|
||||
Remove func(name string) error // Remove file
|
||||
IDs chronograf.ID // IDs generate unique ids for new dashboards
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// NewDashboards constructs a dashboard store wrapping a file system directory
|
||||
func NewDashboards(dir string, ids chronograf.ID, logger chronograf.Logger) chronograf.DashboardsStore {
|
||||
return &Dashboards{
|
||||
Dir: dir,
|
||||
Load: load,
|
||||
Create: create,
|
||||
ReadDir: ioutil.ReadDir,
|
||||
Remove: os.Remove,
|
||||
IDs: ids,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func dashboardFile(dir string, dashboard chronograf.Dashboard) string {
|
||||
base := fmt.Sprintf("%s%s", dashboard.Name, DashExt)
|
||||
return path.Join(dir, base)
|
||||
}
|
||||
|
||||
func load(name string, resource interface{}) error {
|
||||
octets, err := templatedFromEnv(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resource %s not found", name)
|
||||
}
|
||||
|
||||
return json.Unmarshal(octets, resource)
|
||||
}
|
||||
|
||||
func create(file string, resource interface{}) error {
|
||||
h, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer h.Close()
|
||||
|
||||
octets, err := json.MarshalIndent(resource, " ", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = h.Write(octets)
|
||||
return err
|
||||
}
|
||||
|
||||
// All returns all dashboards from the directory
|
||||
func (d *Dashboards) All(ctx context.Context) ([]chronograf.Dashboard, error) {
|
||||
files, err := d.ReadDir(d.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dashboards := []chronograf.Dashboard{}
|
||||
for _, file := range files {
|
||||
if path.Ext(file.Name()) != DashExt {
|
||||
continue
|
||||
}
|
||||
var dashboard chronograf.Dashboard
|
||||
if err := d.Load(path.Join(d.Dir, file.Name()), &dashboard); err != nil {
|
||||
continue // We want to load all files we can.
|
||||
} else {
|
||||
dashboards = append(dashboards, dashboard)
|
||||
}
|
||||
}
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
// Add creates a new dashboard within the directory
|
||||
func (d *Dashboards) Add(ctx context.Context, dashboard chronograf.Dashboard) (chronograf.Dashboard, error) {
|
||||
genID, err := d.IDs.Generate()
|
||||
if err != nil {
|
||||
d.Logger.
|
||||
WithField("component", "dashboard").
|
||||
Error("Unable to generate ID")
|
||||
return chronograf.Dashboard{}, err
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(genID)
|
||||
if err != nil {
|
||||
d.Logger.
|
||||
WithField("component", "dashboard").
|
||||
Error("Unable to convert ID")
|
||||
return chronograf.Dashboard{}, err
|
||||
}
|
||||
|
||||
dashboard.ID = chronograf.DashboardID(id)
|
||||
|
||||
file := dashboardFile(d.Dir, dashboard)
|
||||
if err = d.Create(file, dashboard); err != nil {
|
||||
if err == chronograf.ErrDashboardInvalid {
|
||||
d.Logger.
|
||||
WithField("component", "dashboard").
|
||||
WithField("name", file).
|
||||
Error("Invalid Dashboard: ", err)
|
||||
} else {
|
||||
d.Logger.
|
||||
WithField("component", "dashboard").
|
||||
WithField("name", file).
|
||||
Error("Unable to write dashboard:", err)
|
||||
}
|
||||
return chronograf.Dashboard{}, err
|
||||
}
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
// Delete removes a dashboard file from the directory
|
||||
func (d *Dashboards) Delete(ctx context.Context, dashboard chronograf.Dashboard) error {
|
||||
_, file, err := d.idToFile(dashboard.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.Remove(file); err != nil {
|
||||
d.Logger.
|
||||
WithField("component", "dashboard").
|
||||
WithField("name", file).
|
||||
Error("Unable to remove dashboard:", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns a dashboard file from the dashboard directory
|
||||
func (d *Dashboards) Get(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
board, file, err := d.idToFile(id)
|
||||
if err != nil {
|
||||
if err == chronograf.ErrDashboardNotFound {
|
||||
d.Logger.
|
||||
WithField("component", "dashboard").
|
||||
WithField("name", file).
|
||||
Error("Unable to read file")
|
||||
} else if err == chronograf.ErrDashboardInvalid {
|
||||
d.Logger.
|
||||
WithField("component", "dashboard").
|
||||
WithField("name", file).
|
||||
Error("File is not a dashboard")
|
||||
}
|
||||
return chronograf.Dashboard{}, err
|
||||
}
|
||||
return board, nil
|
||||
}
|
||||
|
||||
// Update replaces a dashboard from the file system directory
|
||||
func (d *Dashboards) Update(ctx context.Context, dashboard chronograf.Dashboard) error {
|
||||
board, _, err := d.idToFile(dashboard.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.Delete(ctx, board); err != nil {
|
||||
return err
|
||||
}
|
||||
file := dashboardFile(d.Dir, dashboard)
|
||||
return d.Create(file, dashboard)
|
||||
}
|
||||
|
||||
// idToFile takes an id and finds the associated filename
|
||||
func (d *Dashboards) idToFile(id chronograf.DashboardID) (chronograf.Dashboard, string, error) {
|
||||
// Because the entire dashboard information is not known at this point, we need
|
||||
// to try to find the name of the file through matching the ID in the dashboard
|
||||
// content with the ID passed.
|
||||
files, err := d.ReadDir(d.Dir)
|
||||
if err != nil {
|
||||
return chronograf.Dashboard{}, "", err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if path.Ext(f.Name()) != DashExt {
|
||||
continue
|
||||
}
|
||||
file := path.Join(d.Dir, f.Name())
|
||||
var dashboard chronograf.Dashboard
|
||||
if err := d.Load(file, &dashboard); err != nil {
|
||||
return chronograf.Dashboard{}, "", err
|
||||
}
|
||||
if dashboard.ID == id {
|
||||
return dashboard, file, nil
|
||||
}
|
||||
}
|
||||
|
||||
return chronograf.Dashboard{}, "", chronograf.ErrDashboardNotFound
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package filestore
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var env map[string]string
|
||||
|
||||
// environ returns a map of all environment variables in the running process
|
||||
func environ() map[string]string {
|
||||
if env == nil {
|
||||
env = make(map[string]string)
|
||||
envVars := os.Environ()
|
||||
for _, envVar := range envVars {
|
||||
kv := strings.SplitN(envVar, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
env[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package filestore
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_environ(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{
|
||||
name: "environment variable is returned",
|
||||
key: "CHRONOGRAF_TEST_ENVIRON",
|
||||
value: "howdy",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
os.Setenv(tt.key, tt.value)
|
||||
got := environ()
|
||||
if v, ok := got[tt.key]; !ok || v != tt.value {
|
||||
t.Errorf("environ() = %v, want %v", v, tt.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
package filestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// KapExt is the the file extension searched for in the directory for kapacitor files
|
||||
const KapExt = ".kap"
|
||||
|
||||
var _ chronograf.ServersStore = &Kapacitors{}
|
||||
|
||||
// Kapacitors are JSON kapacitors stored in the filesystem
|
||||
type Kapacitors struct {
|
||||
Dir string // Dir is the directory containing the kapacitors.
|
||||
Load func(string, interface{}) error // Load loads string name and dashbaord passed in as interface
|
||||
Create func(string, interface{}) error // Create will write kapacitor to file.
|
||||
ReadDir func(dirname string) ([]os.FileInfo, error) // ReadDir reads the directory named by dirname and returns a list of directory entries sorted by filename.
|
||||
Remove func(name string) error // Remove file
|
||||
IDs chronograf.ID // IDs generate unique ids for new kapacitors
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// NewKapacitors constructs a kapacitor store wrapping a file system directory
|
||||
func NewKapacitors(dir string, ids chronograf.ID, logger chronograf.Logger) chronograf.ServersStore {
|
||||
return &Kapacitors{
|
||||
Dir: dir,
|
||||
Load: load,
|
||||
Create: create,
|
||||
ReadDir: ioutil.ReadDir,
|
||||
Remove: os.Remove,
|
||||
IDs: ids,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func kapacitorFile(dir string, kapacitor chronograf.Server) string {
|
||||
base := fmt.Sprintf("%s%s", kapacitor.Name, KapExt)
|
||||
return path.Join(dir, base)
|
||||
}
|
||||
|
||||
// All returns all kapacitors from the directory
|
||||
func (d *Kapacitors) All(ctx context.Context) ([]chronograf.Server, error) {
|
||||
files, err := d.ReadDir(d.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kapacitors := []chronograf.Server{}
|
||||
for _, file := range files {
|
||||
if path.Ext(file.Name()) != KapExt {
|
||||
continue
|
||||
}
|
||||
var kapacitor chronograf.Server
|
||||
if err := d.Load(path.Join(d.Dir, file.Name()), &kapacitor); err != nil {
|
||||
continue // We want to load all files we can.
|
||||
} else {
|
||||
kapacitors = append(kapacitors, kapacitor)
|
||||
}
|
||||
}
|
||||
return kapacitors, nil
|
||||
}
|
||||
|
||||
// Add creates a new kapacitor within the directory
|
||||
func (d *Kapacitors) Add(ctx context.Context, kapacitor chronograf.Server) (chronograf.Server, error) {
|
||||
genID, err := d.IDs.Generate()
|
||||
if err != nil {
|
||||
d.Logger.
|
||||
WithField("component", "kapacitor").
|
||||
Error("Unable to generate ID")
|
||||
return chronograf.Server{}, err
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(genID)
|
||||
if err != nil {
|
||||
d.Logger.
|
||||
WithField("component", "kapacitor").
|
||||
Error("Unable to convert ID")
|
||||
return chronograf.Server{}, err
|
||||
}
|
||||
|
||||
kapacitor.ID = id
|
||||
|
||||
file := kapacitorFile(d.Dir, kapacitor)
|
||||
if err = d.Create(file, kapacitor); err != nil {
|
||||
if err == chronograf.ErrServerInvalid {
|
||||
d.Logger.
|
||||
WithField("component", "kapacitor").
|
||||
WithField("name", file).
|
||||
Error("Invalid Server: ", err)
|
||||
} else {
|
||||
d.Logger.
|
||||
WithField("component", "kapacitor").
|
||||
WithField("name", file).
|
||||
Error("Unable to write kapacitor:", err)
|
||||
}
|
||||
return chronograf.Server{}, err
|
||||
}
|
||||
return kapacitor, nil
|
||||
}
|
||||
|
||||
// Delete removes a kapacitor file from the directory
|
||||
func (d *Kapacitors) Delete(ctx context.Context, kapacitor chronograf.Server) error {
|
||||
_, file, err := d.idToFile(kapacitor.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.Remove(file); err != nil {
|
||||
d.Logger.
|
||||
WithField("component", "kapacitor").
|
||||
WithField("name", file).
|
||||
Error("Unable to remove kapacitor:", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns a kapacitor file from the kapacitor directory
|
||||
func (d *Kapacitors) Get(ctx context.Context, id int) (chronograf.Server, error) {
|
||||
board, file, err := d.idToFile(id)
|
||||
if err != nil {
|
||||
if err == chronograf.ErrServerNotFound {
|
||||
d.Logger.
|
||||
WithField("component", "kapacitor").
|
||||
WithField("name", file).
|
||||
Error("Unable to read file")
|
||||
} else if err == chronograf.ErrServerInvalid {
|
||||
d.Logger.
|
||||
WithField("component", "kapacitor").
|
||||
WithField("name", file).
|
||||
Error("File is not a kapacitor")
|
||||
}
|
||||
return chronograf.Server{}, err
|
||||
}
|
||||
return board, nil
|
||||
}
|
||||
|
||||
// Update replaces a kapacitor from the file system directory
|
||||
func (d *Kapacitors) Update(ctx context.Context, kapacitor chronograf.Server) error {
|
||||
board, _, err := d.idToFile(kapacitor.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.Delete(ctx, board); err != nil {
|
||||
return err
|
||||
}
|
||||
file := kapacitorFile(d.Dir, kapacitor)
|
||||
return d.Create(file, kapacitor)
|
||||
}
|
||||
|
||||
// idToFile takes an id and finds the associated filename
|
||||
func (d *Kapacitors) idToFile(id int) (chronograf.Server, string, error) {
|
||||
// Because the entire kapacitor information is not known at this point, we need
|
||||
// to try to find the name of the file through matching the ID in the kapacitor
|
||||
// content with the ID passed.
|
||||
files, err := d.ReadDir(d.Dir)
|
||||
if err != nil {
|
||||
return chronograf.Server{}, "", err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if path.Ext(f.Name()) != KapExt {
|
||||
continue
|
||||
}
|
||||
file := path.Join(d.Dir, f.Name())
|
||||
var kapacitor chronograf.Server
|
||||
if err := d.Load(file, &kapacitor); err != nil {
|
||||
return chronograf.Server{}, "", err
|
||||
}
|
||||
if kapacitor.ID == id {
|
||||
return kapacitor, file, nil
|
||||
}
|
||||
}
|
||||
|
||||
return chronograf.Server{}, "", chronograf.ErrServerNotFound
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package filestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// OrgExt is the the file extension searched for in the directory for org files
|
||||
const OrgExt = ".org"
|
||||
|
||||
var _ chronograf.OrganizationsStore = &Organizations{}
|
||||
|
||||
// Organizations are JSON orgs stored in the filesystem
|
||||
type Organizations struct {
|
||||
Dir string // Dir is the directory containing the orgs.
|
||||
Load func(string, interface{}) error // Load loads string name and org passed in as interface
|
||||
ReadDir func(dirname string) ([]os.FileInfo, error) // ReadDir reads the directory named by dirname and returns a list of directory entries sorted by filename.
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// NewOrganizations constructs a org store wrapping a file system directory
|
||||
func NewOrganizations(dir string, logger chronograf.Logger) chronograf.OrganizationsStore {
|
||||
return &Organizations{
|
||||
Dir: dir,
|
||||
Load: load,
|
||||
ReadDir: ioutil.ReadDir,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func orgFile(dir string, org chronograf.Organization) string {
|
||||
base := fmt.Sprintf("%s%s", org.Name, OrgExt)
|
||||
return path.Join(dir, base)
|
||||
}
|
||||
|
||||
// All returns all orgs from the directory
|
||||
func (o *Organizations) All(ctx context.Context) ([]chronograf.Organization, error) {
|
||||
files, err := o.ReadDir(o.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgs := []chronograf.Organization{}
|
||||
for _, file := range files {
|
||||
if path.Ext(file.Name()) != OrgExt {
|
||||
continue
|
||||
}
|
||||
var org chronograf.Organization
|
||||
if err := o.Load(path.Join(o.Dir, file.Name()), &org); err != nil {
|
||||
continue // We want to load all files we can.
|
||||
} else {
|
||||
orgs = append(orgs, org)
|
||||
}
|
||||
}
|
||||
return orgs, nil
|
||||
}
|
||||
|
||||
// Get returns a org file from the org directory
|
||||
func (o *Organizations) Get(ctx context.Context, query chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
org, _, err := o.findOrg(query)
|
||||
return org, err
|
||||
}
|
||||
|
||||
// Add is not allowed for the filesystem organization store
|
||||
func (o *Organizations) Add(ctx context.Context, org *chronograf.Organization) (*chronograf.Organization, error) {
|
||||
return nil, fmt.Errorf("unable to add organizations to the filesystem")
|
||||
}
|
||||
|
||||
// Delete is not allowed for the filesystem organization store
|
||||
func (o *Organizations) Delete(ctx context.Context, org *chronograf.Organization) error {
|
||||
return fmt.Errorf("unable to delete an organization from the filesystem")
|
||||
}
|
||||
|
||||
// Update is not allowed for the filesystem organization store
|
||||
func (o *Organizations) Update(ctx context.Context, org *chronograf.Organization) error {
|
||||
return fmt.Errorf("unable to update organizations on the filesystem")
|
||||
}
|
||||
|
||||
// CreateDefault is not allowed for the filesystem organization store
|
||||
func (o *Organizations) CreateDefault(ctx context.Context) error {
|
||||
return fmt.Errorf("unable to create default organizations on the filesystem")
|
||||
}
|
||||
|
||||
// DefaultOrganization is not allowed for the filesystem organization store
|
||||
func (o *Organizations) DefaultOrganization(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return nil, fmt.Errorf("unable to get default organizations from the filestore")
|
||||
}
|
||||
|
||||
// findOrg takes an OrganizationQuery and finds the associated filename
|
||||
func (o *Organizations) findOrg(query chronograf.OrganizationQuery) (*chronograf.Organization, string, error) {
|
||||
// Because the entire org information is not known at this point, we need
|
||||
// to try to find the name of the file through matching the ID or name in the org
|
||||
// content with the ID passed.
|
||||
files, err := o.ReadDir(o.Dir)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if path.Ext(f.Name()) != OrgExt {
|
||||
continue
|
||||
}
|
||||
file := path.Join(o.Dir, f.Name())
|
||||
var org chronograf.Organization
|
||||
if err := o.Load(file, &org); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if query.ID != nil && org.ID == *query.ID {
|
||||
return &org, file, nil
|
||||
}
|
||||
if query.Name != nil && org.Name == *query.Name {
|
||||
return &org, file, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", chronograf.ErrOrganizationNotFound
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
package filestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// SrcExt is the the file extension searched for in the directory for source files
|
||||
const SrcExt = ".src"
|
||||
|
||||
var _ chronograf.SourcesStore = &Sources{}
|
||||
|
||||
// Sources are JSON sources stored in the filesystem
|
||||
type Sources struct {
|
||||
Dir string // Dir is the directory containing the sources.
|
||||
Load func(string, interface{}) error // Load loads string name and dashbaord passed in as interface
|
||||
Create func(string, interface{}) error // Create will write source to file.
|
||||
ReadDir func(dirname string) ([]os.FileInfo, error) // ReadDir reads the directory named by dirname and returns a list of directory entries sorted by filename.
|
||||
Remove func(name string) error // Remove file
|
||||
IDs chronograf.ID // IDs generate unique ids for new sources
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
|
||||
// NewSources constructs a source store wrapping a file system directory
|
||||
func NewSources(dir string, ids chronograf.ID, logger chronograf.Logger) chronograf.SourcesStore {
|
||||
return &Sources{
|
||||
Dir: dir,
|
||||
Load: load,
|
||||
Create: create,
|
||||
ReadDir: ioutil.ReadDir,
|
||||
Remove: os.Remove,
|
||||
IDs: ids,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func sourceFile(dir string, source chronograf.Source) string {
|
||||
base := fmt.Sprintf("%s%s", source.Name, SrcExt)
|
||||
return path.Join(dir, base)
|
||||
}
|
||||
|
||||
// All returns all sources from the directory
|
||||
func (d *Sources) All(ctx context.Context) ([]chronograf.Source, error) {
|
||||
files, err := d.ReadDir(d.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sources := []chronograf.Source{}
|
||||
for _, file := range files {
|
||||
if path.Ext(file.Name()) != SrcExt {
|
||||
continue
|
||||
}
|
||||
var source chronograf.Source
|
||||
if err := d.Load(path.Join(d.Dir, file.Name()), &source); err != nil {
|
||||
continue // We want to load all files we can.
|
||||
} else {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
// Add creates a new source within the directory
|
||||
func (d *Sources) Add(ctx context.Context, source chronograf.Source) (chronograf.Source, error) {
|
||||
genID, err := d.IDs.Generate()
|
||||
if err != nil {
|
||||
d.Logger.
|
||||
WithField("component", "source").
|
||||
Error("Unable to generate ID")
|
||||
return chronograf.Source{}, err
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(genID)
|
||||
if err != nil {
|
||||
d.Logger.
|
||||
WithField("component", "source").
|
||||
Error("Unable to convert ID")
|
||||
return chronograf.Source{}, err
|
||||
}
|
||||
|
||||
source.ID = id
|
||||
|
||||
file := sourceFile(d.Dir, source)
|
||||
if err = d.Create(file, source); err != nil {
|
||||
if err == chronograf.ErrSourceInvalid {
|
||||
d.Logger.
|
||||
WithField("component", "source").
|
||||
WithField("name", file).
|
||||
Error("Invalid Source: ", err)
|
||||
} else {
|
||||
d.Logger.
|
||||
WithField("component", "source").
|
||||
WithField("name", file).
|
||||
Error("Unable to write source:", err)
|
||||
}
|
||||
return chronograf.Source{}, err
|
||||
}
|
||||
return source, nil
|
||||
}
|
||||
|
||||
// Delete removes a source file from the directory
|
||||
func (d *Sources) Delete(ctx context.Context, source chronograf.Source) error {
|
||||
_, file, err := d.idToFile(source.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.Remove(file); err != nil {
|
||||
d.Logger.
|
||||
WithField("component", "source").
|
||||
WithField("name", file).
|
||||
Error("Unable to remove source:", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns a source file from the source directory
|
||||
func (d *Sources) Get(ctx context.Context, id int) (chronograf.Source, error) {
|
||||
board, file, err := d.idToFile(id)
|
||||
if err != nil {
|
||||
if err == chronograf.ErrSourceNotFound {
|
||||
d.Logger.
|
||||
WithField("component", "source").
|
||||
WithField("name", file).
|
||||
Error("Unable to read file")
|
||||
} else if err == chronograf.ErrSourceInvalid {
|
||||
d.Logger.
|
||||
WithField("component", "source").
|
||||
WithField("name", file).
|
||||
Error("File is not a source")
|
||||
}
|
||||
return chronograf.Source{}, err
|
||||
}
|
||||
return board, nil
|
||||
}
|
||||
|
||||
// Update replaces a source from the file system directory
|
||||
func (d *Sources) Update(ctx context.Context, source chronograf.Source) error {
|
||||
board, _, err := d.idToFile(source.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.Delete(ctx, board); err != nil {
|
||||
return err
|
||||
}
|
||||
file := sourceFile(d.Dir, source)
|
||||
return d.Create(file, source)
|
||||
}
|
||||
|
||||
// idToFile takes an id and finds the associated filename
|
||||
func (d *Sources) idToFile(id int) (chronograf.Source, string, error) {
|
||||
// Because the entire source information is not known at this point, we need
|
||||
// to try to find the name of the file through matching the ID in the source
|
||||
// content with the ID passed.
|
||||
files, err := d.ReadDir(d.Dir)
|
||||
if err != nil {
|
||||
return chronograf.Source{}, "", err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if path.Ext(f.Name()) != SrcExt {
|
||||
continue
|
||||
}
|
||||
file := path.Join(d.Dir, f.Name())
|
||||
var source chronograf.Source
|
||||
if err := d.Load(file, &source); err != nil {
|
||||
return chronograf.Source{}, "", err
|
||||
}
|
||||
if source.ID == id {
|
||||
return source, file, nil
|
||||
}
|
||||
}
|
||||
|
||||
return chronograf.Source{}, "", chronograf.ErrSourceNotFound
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package filestore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
// templated returns all files templated using data
|
||||
func templated(data interface{}, filenames ...string) ([]byte, error) {
|
||||
t, err := template.ParseFiles(filenames...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var b bytes.Buffer
|
||||
// If a key in the file exists but is not in the data we
|
||||
// immediately fail with a missing key error
|
||||
err = t.Option("missingkey=error").Execute(&b, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// templatedFromEnv returns all files templated against environment variables
|
||||
func templatedFromEnv(filenames ...string) ([]byte, error) {
|
||||
return templated(environ(), filenames...)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package filestore
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_templated(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content []string
|
||||
data interface{}
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "files with templates are rendered correctly",
|
||||
content: []string{
|
||||
"{{ .MYVAR }}",
|
||||
},
|
||||
data: map[string]string{
|
||||
"MYVAR": "howdy",
|
||||
},
|
||||
want: []byte("howdy"),
|
||||
},
|
||||
{
|
||||
name: "missing key gives an error",
|
||||
content: []string{
|
||||
"{{ .MYVAR }}",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no files make me an error!",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filenames := make([]string, len(tt.content))
|
||||
for i, c := range tt.content {
|
||||
f, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := f.Write([]byte(c)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filenames[i] = f.Name()
|
||||
defer os.Remove(f.Name())
|
||||
}
|
||||
got, err := templated(tt.data, filenames...)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("templated() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("templated() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package id
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// tm generates an id based on current time
|
||||
type tm struct {
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// NewTime builds a chronograf.ID generator based on current time
|
||||
func NewTime() chronograf.ID {
|
||||
return &tm{
|
||||
Now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate creates a string based on the current time as an integer
|
||||
func (i *tm) Generate() (string, error) {
|
||||
return strconv.Itoa(int(i.Now().Unix())), nil
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package id
|
||||
|
||||
import (
|
||||
"github.com/influxdata/chronograf"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
var _ chronograf.ID = &UUID{}
|
||||
|
||||
// UUID generates a V4 uuid
|
||||
type UUID struct{}
|
||||
|
||||
// Generate creates a UUID v4 string
|
||||
func (i *UUID) Generate() (string, error) {
|
||||
return uuid.NewV4().String(), nil
|
||||
}
|
|
@ -38,6 +38,12 @@ func RenderTemplate(query string, t chronograf.TemplateVar, now time.Time) (stri
|
|||
if len(t.Values) == 0 {
|
||||
return query, nil
|
||||
}
|
||||
|
||||
// we only need to render the template if the template exists in the query
|
||||
if !strings.Contains(query, t.Var) {
|
||||
return query, nil
|
||||
}
|
||||
|
||||
switch t.Values[0].Type {
|
||||
case "tagKey", "fieldKey", "measurement", "database":
|
||||
return strings.Replace(query, t.Var, `"`+t.Values[0].Value+`"`, -1), nil
|
||||
|
|
|
@ -62,10 +62,10 @@ func TestTemplateReplace(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "select with parameters and aggregates",
|
||||
query: `SELECT mean($field) FROM "cpu" WHERE $tag = $value GROUP BY $tag`,
|
||||
query: `SELECT mean(:field:) FROM "cpu" WHERE :tag: = :value: GROUP BY :tag:`,
|
||||
vars: []chronograf.TemplateVar{
|
||||
chronograf.TemplateVar{
|
||||
Var: "$value",
|
||||
Var: ":value:",
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Type: "tagValue",
|
||||
|
@ -74,7 +74,7 @@ func TestTemplateReplace(t *testing.T) {
|
|||
},
|
||||
},
|
||||
chronograf.TemplateVar{
|
||||
Var: "$tag",
|
||||
Var: ":tag:",
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Type: "tagKey",
|
||||
|
@ -83,7 +83,7 @@ func TestTemplateReplace(t *testing.T) {
|
|||
},
|
||||
},
|
||||
chronograf.TemplateVar{
|
||||
Var: "$field",
|
||||
Var: ":field:",
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Type: "fieldKey",
|
||||
|
@ -96,25 +96,25 @@ func TestTemplateReplace(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Non-existant parameters",
|
||||
query: `SELECT $field FROM "cpu"`,
|
||||
want: `SELECT $field FROM "cpu"`,
|
||||
query: `SELECT :field: FROM "cpu"`,
|
||||
want: `SELECT :field: FROM "cpu"`,
|
||||
},
|
||||
{
|
||||
name: "var without a value",
|
||||
query: `SELECT $field FROM "cpu"`,
|
||||
query: `SELECT :field: FROM "cpu"`,
|
||||
vars: []chronograf.TemplateVar{
|
||||
chronograf.TemplateVar{
|
||||
Var: "$field",
|
||||
Var: ":field:",
|
||||
},
|
||||
},
|
||||
want: `SELECT $field FROM "cpu"`,
|
||||
want: `SELECT :field: FROM "cpu"`,
|
||||
},
|
||||
{
|
||||
name: "var with unknown type",
|
||||
query: `SELECT $field FROM "cpu"`,
|
||||
query: `SELECT :field: FROM "cpu"`,
|
||||
vars: []chronograf.TemplateVar{
|
||||
chronograf.TemplateVar{
|
||||
Var: "$field",
|
||||
Var: ":field:",
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Type: "who knows?",
|
||||
|
@ -123,7 +123,7 @@ func TestTemplateReplace(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
want: `SELECT $field FROM "cpu"`,
|
||||
want: `SELECT :field: FROM "cpu"`,
|
||||
},
|
||||
{
|
||||
name: "auto group by",
|
||||
|
@ -224,6 +224,71 @@ func TestTemplateReplace(t *testing.T) {
|
|||
},
|
||||
want: `SELECT mean(usage_idle) FROM "cpu" WHERE time > now() - 1h GROUP BY time(93s)`,
|
||||
},
|
||||
{
|
||||
name: "no template variables specified",
|
||||
query: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY :interval:`,
|
||||
want: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY :interval:`,
|
||||
},
|
||||
{
|
||||
name: "auto group by failing condition",
|
||||
query: `SELECT mean(usage_idle) FROM "cpu" WHERE time > :dashboardTime: GROUP BY :interval:`,
|
||||
vars: []chronograf.TemplateVar{
|
||||
{
|
||||
Var: ":interval:",
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Value: "115",
|
||||
Type: "resolution",
|
||||
},
|
||||
{
|
||||
Value: "3",
|
||||
Type: "pointsPerPixel",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Var: ":dashboardTime:",
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Value: "now() - 1h",
|
||||
Type: "constant",
|
||||
Selected: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `SELECT mean(usage_idle) FROM "cpu" WHERE time > now() - 1h GROUP BY time(93s)`,
|
||||
},
|
||||
{
|
||||
name: "query with no template variables contained should return query",
|
||||
query: `SHOW DATABASES`,
|
||||
vars: []chronograf.TemplateVar{
|
||||
{
|
||||
Var: ":interval:",
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Value: "115",
|
||||
Type: "resolution",
|
||||
},
|
||||
{
|
||||
Value: "3",
|
||||
Type: "pointsPerPixel",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Var: ":dashboardTime:",
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Value: "now() - 1h",
|
||||
Type: "constant",
|
||||
Selected: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `SHOW DATABASES`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -392,11 +457,16 @@ func Test_RenderTemplate(t *testing.T) {
|
|||
want: "SELECT mean(usage_idle) FROM cpu WHERE time > '1985-10-25T00:01:00Z' and time < '1985-10-25T00:02:00Z' GROUP BY time(179ms)",
|
||||
},
|
||||
{
|
||||
name: "absolute time with nano seconds and zero duraiton",
|
||||
name: "absolute time with nano seconds and zero duration",
|
||||
query: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY :interval:",
|
||||
resolution: 1000,
|
||||
want: "SELECT mean(usage_idle) FROM cpu WHERE time > '2017-07-24T15:33:42.994Z' and time < '2017-07-24T15:33:42.994Z' GROUP BY time(1ms)",
|
||||
},
|
||||
{
|
||||
name: "query should be returned if there are no template variables",
|
||||
query: "SHOW DATABASES",
|
||||
want: "SHOW DATABASES",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range gbvTests {
|
||||
|
@ -426,5 +496,3 @@ func Test_RenderTemplate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SELECT mean("numSeries") AS "mean_numSeries" FROM "_internal"."monitor"."database" WHERE time > now() - 1h GROUP BY :interval: FILL(null);SELECT mean("numSeries") AS "mean_numSeries_shifted__1__h" FROM "_internal"."monitor"."database" WHERE time > now() - 1h - 1h AND time < now() - 1h GROUP BY :interval: FILL(null)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": 5000,
|
||||
"srcID": 5000,
|
||||
"name": "Kapa 1",
|
||||
"url": "http://localhost:9092",
|
||||
"active": true,
|
||||
"organization": "howdy"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "howdy",
|
||||
"name": "An Organization",
|
||||
"defaultRole": "viewer"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "5000",
|
||||
"name": "Influx 1",
|
||||
"username": "user1",
|
||||
"password": "pass1",
|
||||
"url": "http://localhost:8086",
|
||||
"metaUrl": "http://metaurl.com",
|
||||
"type": "influx-enterprise",
|
||||
"insecureSkipVerify": false,
|
||||
"default": true,
|
||||
"telegraf": "telegraf",
|
||||
"sharedSecret": "cubeapples",
|
||||
"organization": "howdy"
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
{
|
||||
"id": 1000,
|
||||
"cells": [
|
||||
{
|
||||
"i": "8f61c619-dd9b-4761-8aa8-577f27247093",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 11,
|
||||
"h": 5,
|
||||
"name": "Untitled Cell",
|
||||
"queries": [
|
||||
{
|
||||
"query": "SELECT mean(\"value\") AS \"mean_value\" FROM \"telegraf\".\"autogen\".\"cpg\" WHERE time \u003e :dashboardTime: GROUP BY :interval: FILL(null)",
|
||||
"queryConfig": {
|
||||
"id": "b20baa61-bacb-4a17-b27d-b904a0d18114",
|
||||
"database": "telegraf",
|
||||
"measurement": "cpg",
|
||||
"retentionPolicy": "autogen",
|
||||
"fields": [
|
||||
{
|
||||
"value": "mean",
|
||||
"type": "func",
|
||||
"alias": "mean_value",
|
||||
"args": [
|
||||
{
|
||||
"value": "value",
|
||||
"type": "field",
|
||||
"alias": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tags": {},
|
||||
"groupBy": {
|
||||
"time": "auto",
|
||||
"tags": []
|
||||
},
|
||||
"areTagsAccepted": true,
|
||||
"fill": "null",
|
||||
"rawText": null,
|
||||
"range": null,
|
||||
"shifts": []
|
||||
},
|
||||
"source": "/chronograf/v1/sources/2"
|
||||
}
|
||||
],
|
||||
"axes": {
|
||||
"x": {
|
||||
"bounds": [],
|
||||
"label": "",
|
||||
"prefix": "",
|
||||
"suffix": "",
|
||||
"base": "10",
|
||||
"scale": "linear"
|
||||
},
|
||||
"y": {
|
||||
"bounds": [],
|
||||
"label": "",
|
||||
"prefix": "",
|
||||
"suffix": "",
|
||||
"base": "10",
|
||||
"scale": "linear"
|
||||
},
|
||||
"y2": {
|
||||
"bounds": [],
|
||||
"label": "",
|
||||
"prefix": "",
|
||||
"suffix": "",
|
||||
"base": "10",
|
||||
"scale": "linear"
|
||||
}
|
||||
},
|
||||
"type": "line",
|
||||
"colors": [
|
||||
{
|
||||
"id": "0",
|
||||
"type": "min",
|
||||
"hex": "#00C9FF",
|
||||
"name": "laser",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
"type": "max",
|
||||
"hex": "#9394FF",
|
||||
"name": "comet",
|
||||
"value": "100"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"templates": [
|
||||
{
|
||||
"tempVar": ":dbs:",
|
||||
"values": [
|
||||
{
|
||||
"value": "_internal",
|
||||
"type": "database",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"value": "telegraf",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "tensorflowdb",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "pushgateway",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "node_exporter",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "mydb",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "tiny",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "blah",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "test",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "chronograf",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "db_name",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "demo",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "eeg",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "solaredge",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"value": "zipkin",
|
||||
"type": "database",
|
||||
"selected": false
|
||||
}
|
||||
],
|
||||
"id": "e7e498bf-5869-4874-9071-24628a2cda63",
|
||||
"type": "databases",
|
||||
"label": "",
|
||||
"query": {
|
||||
"influxql": "SHOW DATABASES",
|
||||
"measurement": "",
|
||||
"tagKey": "",
|
||||
"fieldKey": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"name": "Name This Dashboard",
|
||||
"organization": "howdy"
|
||||
}
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/uuid"
|
||||
"github.com/influxdata/chronograf/id"
|
||||
client "github.com/influxdata/kapacitor/client/v1"
|
||||
)
|
||||
|
||||
|
@ -44,7 +44,7 @@ func NewClient(url, username, password string, insecureSkipVerify bool) *Client
|
|||
Username: username,
|
||||
Password: password,
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
ID: &uuid.V4{},
|
||||
ID: &id.UUID{},
|
||||
Ticker: &Alert{},
|
||||
kapaClient: NewKapaClient,
|
||||
}
|
||||
|
|
|
@ -7,9 +7,8 @@ import (
|
|||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// Ensure KapacitorStore and MultiKapacitorStore implements chronograf.ServersStore.
|
||||
// Ensure KapacitorStore implements chronograf.ServersStore.
|
||||
var _ chronograf.ServersStore = &KapacitorStore{}
|
||||
var _ chronograf.ServersStore = &MultiKapacitorStore{}
|
||||
|
||||
// KapacitorStore implements the chronograf.ServersStore interface, and keeps
|
||||
// an in-memory Kapacitor according to startup configuration
|
||||
|
@ -55,90 +54,3 @@ func (store *KapacitorStore) Update(ctx context.Context, kap chronograf.Server)
|
|||
store.Kapacitor = &kap
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultiKapacitorStore implements the chronograf.ServersStore interface, and
|
||||
// delegates to all contained KapacitorStores
|
||||
type MultiKapacitorStore struct {
|
||||
Stores []chronograf.ServersStore
|
||||
}
|
||||
|
||||
// All concatenates the Kapacitors of all contained Stores
|
||||
func (multi *MultiKapacitorStore) All(ctx context.Context) ([]chronograf.Server, error) {
|
||||
all := []chronograf.Server{}
|
||||
kapSet := map[int]struct{}{}
|
||||
|
||||
ok := false
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var kaps []chronograf.Server
|
||||
kaps, err = store.All(ctx)
|
||||
if err != nil {
|
||||
// If this Store is unable to return an array of kapacitors, skip to the
|
||||
// next Store.
|
||||
continue
|
||||
}
|
||||
ok = true // We've received a response from at least one Store
|
||||
for _, kap := range kaps {
|
||||
// Enforce that the kapacitor has a unique ID
|
||||
// If the ID has been seen before, ignore the kapacitor
|
||||
if _, okay := kapSet[kap.ID]; !okay { // We have a new kapacitor
|
||||
kapSet[kap.ID] = struct{}{} // We just care that the ID is unique
|
||||
all = append(all, kap)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Add the kap to the first responsive Store
|
||||
func (multi *MultiKapacitorStore) Add(ctx context.Context, kap chronograf.Server) (chronograf.Server, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var k chronograf.Server
|
||||
k, err = store.Add(ctx, kap)
|
||||
if err == nil {
|
||||
return k, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Server{}, nil
|
||||
}
|
||||
|
||||
// Delete delegates to all Stores, returns success if one Store is successful
|
||||
func (multi *MultiKapacitorStore) Delete(ctx context.Context, kap chronograf.Server) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Delete(ctx, kap)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get finds the Source by id among all contained Stores
|
||||
func (multi *MultiKapacitorStore) Get(ctx context.Context, id int) (chronograf.Server, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var k chronograf.Server
|
||||
k, err = store.Get(ctx, id)
|
||||
if err == nil {
|
||||
return k, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Server{}, nil
|
||||
}
|
||||
|
||||
// Update the first responsive Store
|
||||
func (multi *MultiKapacitorStore) Update(ctx context.Context, kap chronograf.Server) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Update(ctx, kap)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
|
||||
func TestInterfaceImplementation(t *testing.T) {
|
||||
var _ chronograf.ServersStore = &KapacitorStore{}
|
||||
var _ chronograf.ServersStore = &MultiKapacitorStore{}
|
||||
}
|
||||
|
||||
func TestKapacitorStoreAll(t *testing.T) {
|
||||
|
|
|
@ -7,95 +7,8 @@ import (
|
|||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// Ensure MultiSourcesStore and SourcesStore implements chronograf.SourcesStore.
|
||||
// Ensure SourcesStore implements chronograf.SourcesStore.
|
||||
var _ chronograf.SourcesStore = &SourcesStore{}
|
||||
var _ chronograf.SourcesStore = &MultiSourcesStore{}
|
||||
|
||||
// MultiSourcesStore delegates to the SourcesStores that compose it
|
||||
type MultiSourcesStore struct {
|
||||
Stores []chronograf.SourcesStore
|
||||
}
|
||||
|
||||
// All concatenates the Sources of all contained Stores
|
||||
func (multi *MultiSourcesStore) All(ctx context.Context) ([]chronograf.Source, error) {
|
||||
all := []chronograf.Source{}
|
||||
sourceSet := map[int]struct{}{}
|
||||
|
||||
ok := false
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var sources []chronograf.Source
|
||||
sources, err = store.All(ctx)
|
||||
if err != nil {
|
||||
// If this Store is unable to return an array of sources, skip to the
|
||||
// next Store.
|
||||
continue
|
||||
}
|
||||
ok = true // We've received a response from at least one Store
|
||||
for _, s := range sources {
|
||||
// Enforce that the source has a unique ID
|
||||
// If the source has been seen before, don't override what we already have
|
||||
if _, okay := sourceSet[s.ID]; !okay { // We have a new Source!
|
||||
sourceSet[s.ID] = struct{}{} // We just care that the ID is unique
|
||||
all = append(all, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Add the src to the first Store to respond successfully
|
||||
func (multi *MultiSourcesStore) Add(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var s chronograf.Source
|
||||
s, err = store.Add(ctx, src)
|
||||
if err == nil {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Source{}, nil
|
||||
}
|
||||
|
||||
// Delete delegates to all stores, returns success if one Store is successful
|
||||
func (multi *MultiSourcesStore) Delete(ctx context.Context, src chronograf.Source) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Delete(ctx, src)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get finds the Source by id among all contained Stores
|
||||
func (multi *MultiSourcesStore) Get(ctx context.Context, id int) (chronograf.Source, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var s chronograf.Source
|
||||
s, err = store.Get(ctx, id)
|
||||
if err == nil {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Source{}, err
|
||||
}
|
||||
|
||||
// Update the first store to return a successful response
|
||||
func (multi *MultiSourcesStore) Update(ctx context.Context, src chronograf.Source) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Update(ctx, src)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// SourcesStore implements the chronograf.SourcesStore interface
|
||||
type SourcesStore struct {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// ConfigStore stores global application configuration
|
||||
type ConfigStore struct {
|
||||
Config *chronograf.Config
|
||||
}
|
||||
|
||||
// Initialize is noop in mocks store
|
||||
func (c ConfigStore) Initialize(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the whole global application configuration
|
||||
func (c ConfigStore) Get(ctx context.Context) (*chronograf.Config, error) {
|
||||
return c.Config, nil
|
||||
}
|
||||
|
||||
// Update updates the whole global application configuration
|
||||
func (c ConfigStore) Update(ctx context.Context, config *chronograf.Config) error {
|
||||
c.Config = config
|
||||
return nil
|
||||
}
|
|
@ -8,6 +8,11 @@ import (
|
|||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// NewLogger returns a mock logger that implements chronograf.Logger
|
||||
func NewLogger() chronograf.Logger {
|
||||
return &TestLogger{}
|
||||
}
|
||||
|
||||
type LogMessage struct {
|
||||
Level string
|
||||
Body string
|
||||
|
|
|
@ -14,6 +14,7 @@ type Store struct {
|
|||
UsersStore chronograf.UsersStore
|
||||
DashboardsStore chronograf.DashboardsStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
}
|
||||
|
||||
func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
|
||||
|
@ -39,3 +40,7 @@ func (s *Store) Organizations(ctx context.Context) chronograf.OrganizationsStore
|
|||
func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
|
||||
return s.DashboardsStore
|
||||
}
|
||||
|
||||
func (s *Store) Config(ctx context.Context) chronograf.ConfigStore {
|
||||
return s.ConfigStore
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
package multistore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// Ensure DashboardsStore implements chronograf.DashboardsStore.
|
||||
var _ chronograf.DashboardsStore = &DashboardsStore{}
|
||||
|
||||
// DashboardsStore implements the chronograf.DashboardsStore interface, and
|
||||
// delegates to all contained DashboardsStores
|
||||
type DashboardsStore struct {
|
||||
Stores []chronograf.DashboardsStore
|
||||
}
|
||||
|
||||
// All concatenates the Dashboards of all contained Stores
|
||||
func (multi *DashboardsStore) All(ctx context.Context) ([]chronograf.Dashboard, error) {
|
||||
all := []chronograf.Dashboard{}
|
||||
boardSet := map[chronograf.DashboardID]struct{}{}
|
||||
|
||||
ok := false
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var boards []chronograf.Dashboard
|
||||
boards, err = store.All(ctx)
|
||||
if err != nil {
|
||||
// If this Store is unable to return an array of dashboards, skip to the
|
||||
// next Store.
|
||||
continue
|
||||
}
|
||||
ok = true // We've received a response from at least one Store
|
||||
for _, board := range boards {
|
||||
// Enforce that the dashboard has a unique ID
|
||||
// If the ID has been seen before, ignore the dashboard
|
||||
if _, okay := boardSet[board.ID]; !okay { // We have a new dashboard
|
||||
boardSet[board.ID] = struct{}{} // We just care that the ID is unique
|
||||
all = append(all, board)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Add the dashboard to the first responsive Store
|
||||
func (multi *DashboardsStore) Add(ctx context.Context, dashboard chronograf.Dashboard) (chronograf.Dashboard, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var d chronograf.Dashboard
|
||||
d, err = store.Add(ctx, dashboard)
|
||||
if err == nil {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Dashboard{}, nil
|
||||
}
|
||||
|
||||
// Delete delegates to all Stores, returns success if one Store is successful
|
||||
func (multi *DashboardsStore) Delete(ctx context.Context, dashboard chronograf.Dashboard) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Delete(ctx, dashboard)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get finds the Dashboard by id among all contained Stores
|
||||
func (multi *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var d chronograf.Dashboard
|
||||
d, err = store.Get(ctx, id)
|
||||
if err == nil {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Dashboard{}, nil
|
||||
}
|
||||
|
||||
// Update the first responsive Store
|
||||
func (multi *DashboardsStore) Update(ctx context.Context, dashboard chronograf.Dashboard) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Update(ctx, dashboard)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package multistore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// Ensure KapacitorStore implements chronograf.ServersStore.
|
||||
var _ chronograf.ServersStore = &KapacitorStore{}
|
||||
|
||||
// KapacitorStore implements the chronograf.ServersStore interface, and
|
||||
// delegates to all contained KapacitorStores
|
||||
type KapacitorStore struct {
|
||||
Stores []chronograf.ServersStore
|
||||
}
|
||||
|
||||
// All concatenates the Kapacitors of all contained Stores
|
||||
func (multi *KapacitorStore) All(ctx context.Context) ([]chronograf.Server, error) {
|
||||
all := []chronograf.Server{}
|
||||
kapSet := map[int]struct{}{}
|
||||
|
||||
ok := false
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var kaps []chronograf.Server
|
||||
kaps, err = store.All(ctx)
|
||||
if err != nil {
|
||||
// If this Store is unable to return an array of kapacitors, skip to the
|
||||
// next Store.
|
||||
continue
|
||||
}
|
||||
ok = true // We've received a response from at least one Store
|
||||
for _, kap := range kaps {
|
||||
// Enforce that the kapacitor has a unique ID
|
||||
// If the ID has been seen before, ignore the kapacitor
|
||||
if _, okay := kapSet[kap.ID]; !okay { // We have a new kapacitor
|
||||
kapSet[kap.ID] = struct{}{} // We just care that the ID is unique
|
||||
all = append(all, kap)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Add the kap to the first responsive Store
|
||||
func (multi *KapacitorStore) Add(ctx context.Context, kap chronograf.Server) (chronograf.Server, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var k chronograf.Server
|
||||
k, err = store.Add(ctx, kap)
|
||||
if err == nil {
|
||||
return k, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Server{}, nil
|
||||
}
|
||||
|
||||
// Delete delegates to all Stores, returns success if one Store is successful
|
||||
func (multi *KapacitorStore) Delete(ctx context.Context, kap chronograf.Server) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Delete(ctx, kap)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get finds the Source by id among all contained Stores
|
||||
func (multi *KapacitorStore) Get(ctx context.Context, id int) (chronograf.Server, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var k chronograf.Server
|
||||
k, err = store.Get(ctx, id)
|
||||
if err == nil {
|
||||
return k, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Server{}, nil
|
||||
}
|
||||
|
||||
// Update the first responsive Store
|
||||
func (multi *KapacitorStore) Update(ctx context.Context, kap chronograf.Server) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Update(ctx, kap)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package multistore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
func TestInterfaceImplementation(t *testing.T) {
|
||||
var _ chronograf.ServersStore = &KapacitorStore{}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package layouts
|
||||
package multistore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -6,15 +6,15 @@ import (
|
|||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// MultiLayoutsStore is a LayoutsStore that contains multiple LayoutsStores
|
||||
// Layouts is a LayoutsStore that contains multiple LayoutsStores
|
||||
// The All method will return the set of all Layouts.
|
||||
// Each method will be tried against the Stores slice serially.
|
||||
type MultiLayoutsStore struct {
|
||||
type Layouts struct {
|
||||
Stores []chronograf.LayoutsStore
|
||||
}
|
||||
|
||||
// All returns the set of all layouts
|
||||
func (s *MultiLayoutsStore) All(ctx context.Context) ([]chronograf.Layout, error) {
|
||||
func (s *Layouts) All(ctx context.Context) ([]chronograf.Layout, error) {
|
||||
all := []chronograf.Layout{}
|
||||
layoutSet := map[string]chronograf.Layout{}
|
||||
ok := false
|
||||
|
@ -43,7 +43,7 @@ func (s *MultiLayoutsStore) All(ctx context.Context) ([]chronograf.Layout, error
|
|||
}
|
||||
|
||||
// Add creates a new dashboard in the LayoutsStore. Tries each store sequentially until success.
|
||||
func (s *MultiLayoutsStore) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
|
||||
func (s *Layouts) Add(ctx context.Context, layout chronograf.Layout) (chronograf.Layout, error) {
|
||||
var err error
|
||||
for _, store := range s.Stores {
|
||||
var l chronograf.Layout
|
||||
|
@ -57,7 +57,7 @@ func (s *MultiLayoutsStore) Add(ctx context.Context, layout chronograf.Layout) (
|
|||
|
||||
// Delete the dashboard from the store. Searches through all stores to find Layout and
|
||||
// then deletes from that store.
|
||||
func (s *MultiLayoutsStore) Delete(ctx context.Context, layout chronograf.Layout) error {
|
||||
func (s *Layouts) Delete(ctx context.Context, layout chronograf.Layout) error {
|
||||
var err error
|
||||
for _, store := range s.Stores {
|
||||
err = store.Delete(ctx, layout)
|
||||
|
@ -69,7 +69,7 @@ func (s *MultiLayoutsStore) Delete(ctx context.Context, layout chronograf.Layout
|
|||
}
|
||||
|
||||
// Get retrieves Layout if `ID` exists. Searches through each store sequentially until success.
|
||||
func (s *MultiLayoutsStore) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
|
||||
func (s *Layouts) Get(ctx context.Context, ID string) (chronograf.Layout, error) {
|
||||
var err error
|
||||
for _, store := range s.Stores {
|
||||
var l chronograf.Layout
|
||||
|
@ -82,7 +82,7 @@ func (s *MultiLayoutsStore) Get(ctx context.Context, ID string) (chronograf.Layo
|
|||
}
|
||||
|
||||
// Update the dashboard in the store. Searches through each store sequentially until success.
|
||||
func (s *MultiLayoutsStore) Update(ctx context.Context, layout chronograf.Layout) error {
|
||||
func (s *Layouts) Update(ctx context.Context, layout chronograf.Layout) error {
|
||||
var err error
|
||||
for _, store := range s.Stores {
|
||||
err = store.Update(ctx, layout)
|
|
@ -0,0 +1,129 @@
|
|||
package multistore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// Ensure OrganizationsStore implements chronograf.OrganizationsStore.
|
||||
var _ chronograf.OrganizationsStore = &OrganizationsStore{}
|
||||
|
||||
// OrganizationsStore implements the chronograf.OrganizationsStore interface, and
|
||||
// delegates to all contained OrganizationsStores
|
||||
type OrganizationsStore struct {
|
||||
Stores []chronograf.OrganizationsStore
|
||||
}
|
||||
|
||||
// All concatenates the Organizations of all contained Stores
|
||||
func (multi *OrganizationsStore) All(ctx context.Context) ([]chronograf.Organization, error) {
|
||||
all := []chronograf.Organization{}
|
||||
orgSet := map[string]struct{}{}
|
||||
|
||||
ok := false
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var orgs []chronograf.Organization
|
||||
orgs, err = store.All(ctx)
|
||||
if err != nil {
|
||||
// If this Store is unable to return an array of orgs, skip to the
|
||||
// next Store.
|
||||
continue
|
||||
}
|
||||
ok = true // We've received a response from at least one Store
|
||||
for _, org := range orgs {
|
||||
// Enforce that the org has a unique ID
|
||||
// If the ID has been seen before, ignore the org
|
||||
if _, okay := orgSet[org.ID]; !okay { // We have a new org
|
||||
orgSet[org.ID] = struct{}{} // We just care that the ID is unique
|
||||
all = append(all, org)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Add the org to the first responsive Store
|
||||
func (multi *OrganizationsStore) Add(ctx context.Context, org *chronograf.Organization) (*chronograf.Organization, error) {
|
||||
errors := []string{}
|
||||
for _, store := range multi.Stores {
|
||||
var o *chronograf.Organization
|
||||
o, err := store.Add(ctx, org)
|
||||
if err == nil {
|
||||
return o, nil
|
||||
}
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
return nil, fmt.Errorf("Unknown error while adding organization: %s", strings.Join(errors, " "))
|
||||
}
|
||||
|
||||
// Delete delegates to all Stores, returns success if one Store is successful
|
||||
func (multi *OrganizationsStore) Delete(ctx context.Context, org *chronograf.Organization) error {
|
||||
errors := []string{}
|
||||
for _, store := range multi.Stores {
|
||||
err := store.Delete(ctx, org)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
return fmt.Errorf("Unknown error while deleting organization: %s", strings.Join(errors, " "))
|
||||
}
|
||||
|
||||
// Get finds the Organization by id among all contained Stores
|
||||
func (multi *OrganizationsStore) Get(ctx context.Context, query chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var o *chronograf.Organization
|
||||
o, err = store.Get(ctx, query)
|
||||
if err == nil {
|
||||
return o, nil
|
||||
}
|
||||
}
|
||||
return nil, chronograf.ErrOrganizationNotFound
|
||||
}
|
||||
|
||||
// Update the first responsive Store
|
||||
func (multi *OrganizationsStore) Update(ctx context.Context, org *chronograf.Organization) error {
|
||||
errors := []string{}
|
||||
for _, store := range multi.Stores {
|
||||
err := store.Update(ctx, org)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
return fmt.Errorf("Unknown error while updating organization: %s", strings.Join(errors, " "))
|
||||
}
|
||||
|
||||
// CreateDefault makes a default organization in the first responsive Store
|
||||
func (multi *OrganizationsStore) CreateDefault(ctx context.Context) error {
|
||||
errors := []string{}
|
||||
for _, store := range multi.Stores {
|
||||
err := store.CreateDefault(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
return fmt.Errorf("Unknown error while creating default organization: %s", strings.Join(errors, " "))
|
||||
}
|
||||
|
||||
// DefaultOrganization returns the first successful DefaultOrganization
|
||||
func (multi *OrganizationsStore) DefaultOrganization(ctx context.Context) (*chronograf.Organization, error) {
|
||||
errors := []string{}
|
||||
for _, store := range multi.Stores {
|
||||
org, err := store.DefaultOrganization(ctx)
|
||||
if err == nil {
|
||||
return org, nil
|
||||
}
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
return nil, fmt.Errorf("Unknown error while getting default organization: %s", strings.Join(errors, " "))
|
||||
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package multistore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// Ensure SourcesStore implements chronograf.SourcesStore.
|
||||
var _ chronograf.SourcesStore = &SourcesStore{}
|
||||
|
||||
// SourcesStore delegates to the SourcesStores that compose it
|
||||
type SourcesStore struct {
|
||||
Stores []chronograf.SourcesStore
|
||||
}
|
||||
|
||||
// All concatenates the Sources of all contained Stores
|
||||
func (multi *SourcesStore) All(ctx context.Context) ([]chronograf.Source, error) {
|
||||
all := []chronograf.Source{}
|
||||
sourceSet := map[int]struct{}{}
|
||||
|
||||
ok := false
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var sources []chronograf.Source
|
||||
sources, err = store.All(ctx)
|
||||
if err != nil {
|
||||
// If this Store is unable to return an array of sources, skip to the
|
||||
// next Store.
|
||||
continue
|
||||
}
|
||||
ok = true // We've received a response from at least one Store
|
||||
for _, s := range sources {
|
||||
// Enforce that the source has a unique ID
|
||||
// If the source has been seen before, don't override what we already have
|
||||
if _, okay := sourceSet[s.ID]; !okay { // We have a new Source!
|
||||
sourceSet[s.ID] = struct{}{} // We just care that the ID is unique
|
||||
all = append(all, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, err
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Add the src to the first Store to respond successfully
|
||||
func (multi *SourcesStore) Add(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var s chronograf.Source
|
||||
s, err = store.Add(ctx, src)
|
||||
if err == nil {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Source{}, nil
|
||||
}
|
||||
|
||||
// Delete delegates to all stores, returns success if one Store is successful
|
||||
func (multi *SourcesStore) Delete(ctx context.Context, src chronograf.Source) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Delete(ctx, src)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get finds the Source by id among all contained Stores
|
||||
func (multi *SourcesStore) Get(ctx context.Context, id int) (chronograf.Source, error) {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
var s chronograf.Source
|
||||
s, err = store.Get(ctx, id)
|
||||
if err == nil {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return chronograf.Source{}, err
|
||||
}
|
||||
|
||||
// Update the first store to return a successful response
|
||||
func (multi *SourcesStore) Update(ctx context.Context, src chronograf.Source) error {
|
||||
var err error
|
||||
for _, store := range multi.Stores {
|
||||
err = store.Update(ctx, src)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package noop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
// ensure ConfigStore implements chronograf.ConfigStore
|
||||
var _ chronograf.ConfigStore = &ConfigStore{}
|
||||
|
||||
type ConfigStore struct{}
|
||||
|
||||
// TODO(desa): this really should be removed
|
||||
func (s *ConfigStore) Initialize(context.Context) error {
|
||||
return fmt.Errorf("cannot initialize")
|
||||
}
|
||||
|
||||
func (s *ConfigStore) Get(context.Context) (*chronograf.Config, error) {
|
||||
return nil, chronograf.ErrConfigNotFound
|
||||
}
|
||||
|
||||
func (s *ConfigStore) Update(context.Context, *chronograf.Config) error {
|
||||
return fmt.Errorf("cannot update conifg")
|
||||
}
|
|
@ -66,13 +66,13 @@ func (s *OrganizationsStore) All(ctx context.Context) ([]chronograf.Organization
|
|||
return nil, err
|
||||
}
|
||||
|
||||
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
|
||||
defaultOrgID := defaultOrg.ID
|
||||
|
||||
// This filters organizations without allocating
|
||||
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
|
||||
organizations := ds[:0]
|
||||
for _, d := range ds {
|
||||
id := fmt.Sprintf("%d", d.ID)
|
||||
id := d.ID
|
||||
switch id {
|
||||
case s.organization, defaultOrgID:
|
||||
organizations = append(organizations, d)
|
||||
|
@ -117,7 +117,7 @@ func (s *OrganizationsStore) Get(ctx context.Context, q chronograf.OrganizationQ
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if fmt.Sprintf("%d", d.ID) != s.organization {
|
||||
if d.ID != s.organization {
|
||||
return nil, chronograf.ErrOrganizationNotFound
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ func TestOrganizations_All(t *testing.T) {
|
|||
},
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
}, nil
|
||||
},
|
||||
|
@ -58,7 +58,7 @@ func TestOrganizations_All(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
}, nil
|
||||
},
|
||||
|
@ -66,11 +66,11 @@ func TestOrganizations_All(t *testing.T) {
|
|||
return []chronograf.Organization{
|
||||
{
|
||||
Name: "howdy",
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
},
|
||||
{
|
||||
Name: "doody",
|
||||
ID: 1447,
|
||||
ID: "1447",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
|
@ -83,11 +83,11 @@ func TestOrganizations_All(t *testing.T) {
|
|||
want: []chronograf.Organization{
|
||||
{
|
||||
Name: "howdy",
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
},
|
||||
{
|
||||
Name: "Default",
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -133,7 +133,7 @@ func TestOrganizations_Add(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1229,
|
||||
ID: "1229",
|
||||
Name: "howdy",
|
||||
}, nil
|
||||
},
|
||||
|
@ -193,7 +193,7 @@ func TestOrganizations_Delete(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1229,
|
||||
ID: "1229",
|
||||
Name: "howdy",
|
||||
}, nil
|
||||
},
|
||||
|
@ -203,7 +203,7 @@ func TestOrganizations_Delete(t *testing.T) {
|
|||
organizationID: "1229",
|
||||
ctx: context.Background(),
|
||||
organization: &chronograf.Organization{
|
||||
ID: 1229,
|
||||
ID: "1229",
|
||||
Name: "howdy",
|
||||
},
|
||||
},
|
||||
|
@ -244,7 +244,7 @@ func TestOrganizations_Get(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "howdy",
|
||||
}, nil
|
||||
},
|
||||
|
@ -254,12 +254,12 @@ func TestOrganizations_Get(t *testing.T) {
|
|||
organizationID: "1337",
|
||||
ctx: context.Background(),
|
||||
organization: &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "howdy",
|
||||
},
|
||||
},
|
||||
want: &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "howdy",
|
||||
},
|
||||
},
|
||||
|
@ -305,7 +305,7 @@ func TestOrganizations_Update(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1229,
|
||||
ID: "1229",
|
||||
Name: "doody",
|
||||
}, nil
|
||||
},
|
||||
|
@ -315,7 +315,7 @@ func TestOrganizations_Update(t *testing.T) {
|
|||
organizationID: "1229",
|
||||
ctx: context.Background(),
|
||||
organization: &chronograf.Organization{
|
||||
ID: 1229,
|
||||
ID: "1229",
|
||||
Name: "howdy",
|
||||
},
|
||||
name: "doody",
|
||||
|
|
|
@ -142,6 +142,20 @@ func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.U
|
|||
// and the user that was found in the underlying store
|
||||
usr.Roles = append(roles, u.Roles...)
|
||||
|
||||
// u.SuperAdmin == true is logically equivalent to u.SuperAdmin, however
|
||||
// it is more clear on a conceptual level to check equality
|
||||
//
|
||||
// TODO(desa): this should go away with https://github.com/influxdata/chronograf/issues/2207
|
||||
// I do not like checking super admin here. The organization users store should only be
|
||||
// concerned about organizations.
|
||||
//
|
||||
// If the user being added already existed in a previous organization, and was already a SuperAdmin,
|
||||
// then this ensures that they retain their SuperAdmin status. And if they weren't a SuperAdmin, and
|
||||
// the user being added has been granted SuperAdmin status, they will be promoted
|
||||
if u.SuperAdmin == true {
|
||||
usr.SuperAdmin = true
|
||||
}
|
||||
|
||||
// Update the user in the underlying store
|
||||
if err := s.store.Update(ctx, usr); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -315,9 +315,62 @@ func TestUsersStore_Add(t *testing.T) {
|
|||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Organization: "1337",
|
||||
Name: "editor",
|
||||
Organization: "1336",
|
||||
Name: "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Add non-new user with Role. Stored user is not super admin. Provided user is super admin",
|
||||
fields: fields{
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
|
||||
return u, nil
|
||||
},
|
||||
UpdateF: func(ctx context.Context, u *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
ID: 1234,
|
||||
Name: "docbrown",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: false,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Organization: "1337",
|
||||
Name: "editor",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
u: &chronograf.User{
|
||||
ID: 1234,
|
||||
Name: "docbrown",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Organization: "1336",
|
||||
Name: "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
orgID: "1336",
|
||||
},
|
||||
want: &chronograf.User{
|
||||
Name: "docbrown",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Organization: "1336",
|
||||
Name: "admin",
|
||||
|
@ -503,6 +556,9 @@ func TestUsersStore_Add(t *testing.T) {
|
|||
if got == nil && tt.want == nil {
|
||||
continue
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.want, userCmpOptions...); diff != "" {
|
||||
t.Errorf("%q. UsersStore.Add():\n-got/+want\ndiff %s", tt.name, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -99,19 +99,13 @@ func AuthorizedUser(
|
|||
Error(w, http.StatusForbidden, "User is not authorized", logger)
|
||||
return
|
||||
}
|
||||
p.Organization = fmt.Sprintf("%d", defaultOrg.ID)
|
||||
p.Organization = defaultOrg.ID
|
||||
}
|
||||
|
||||
// validate that the organization exists
|
||||
orgID, err := parseOrganizationID(p.Organization)
|
||||
_, err = store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization})
|
||||
if err != nil {
|
||||
log.Error("Failed to validate organization on context")
|
||||
Error(w, http.StatusForbidden, "User is not authorized", logger)
|
||||
return
|
||||
}
|
||||
_, err = store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &orgID})
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Failed to retrieve organization %d from organizations store", orgID))
|
||||
log.Error(fmt.Sprintf("Failed to retrieve organization %s from organizations store", p.Organization))
|
||||
Error(w, http.StatusForbidden, "User is not authorized", logger)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
|
@ -133,7 +133,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -141,7 +141,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -189,7 +189,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -197,7 +197,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -245,7 +245,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -253,7 +253,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -301,7 +301,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -309,7 +309,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -353,7 +353,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -361,7 +361,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -409,7 +409,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -417,7 +417,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -465,7 +465,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -473,7 +473,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -517,7 +517,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -525,7 +525,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -569,7 +569,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -577,7 +577,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -620,7 +620,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -628,7 +628,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -667,7 +667,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -675,7 +675,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -714,7 +714,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -722,7 +722,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -765,7 +765,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -773,7 +773,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -819,7 +819,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -862,7 +862,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -870,7 +870,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -914,7 +914,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -922,7 +922,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -966,7 +966,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -974,7 +974,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -1018,7 +1018,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1026,7 +1026,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -1071,7 +1071,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1079,7 +1079,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -1128,7 +1128,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1136,7 +1136,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -1185,7 +1185,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1193,7 +1193,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -1242,7 +1242,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1250,7 +1250,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -1298,7 +1298,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1306,7 +1306,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -1346,7 +1346,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1354,7 +1354,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -1397,7 +1397,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1405,7 +1405,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
@ -1449,7 +1449,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1457,9 +1457,9 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
switch *q.ID {
|
||||
case 1338:
|
||||
case "1338":
|
||||
return &chronograf.Organization{
|
||||
ID: 1338,
|
||||
ID: "1338",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
default:
|
||||
|
@ -1511,7 +1511,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -1519,7 +1519,7 @@ func TestAuthorizedUser(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
}, nil
|
||||
},
|
||||
|
|
|
@ -3,27 +3,28 @@ package server
|
|||
import (
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/canned"
|
||||
"github.com/influxdata/chronograf/layouts"
|
||||
"github.com/influxdata/chronograf/filestore"
|
||||
"github.com/influxdata/chronograf/memdb"
|
||||
"github.com/influxdata/chronograf/multistore"
|
||||
)
|
||||
|
||||
// LayoutBuilder is responsible for building Layouts
|
||||
type LayoutBuilder interface {
|
||||
Build(chronograf.LayoutsStore) (*layouts.MultiLayoutsStore, error)
|
||||
Build(chronograf.LayoutsStore) (*multistore.Layouts, error)
|
||||
}
|
||||
|
||||
// MultiLayoutBuilder implements LayoutBuilder and will return a MultiLayoutsStore
|
||||
// MultiLayoutBuilder implements LayoutBuilder and will return a Layouts
|
||||
type MultiLayoutBuilder struct {
|
||||
Logger chronograf.Logger
|
||||
UUID chronograf.ID
|
||||
CannedPath string
|
||||
}
|
||||
|
||||
// Build will construct a MultiLayoutsStore of canned and db-backed personalized
|
||||
// Build will construct a Layouts of canned and db-backed personalized
|
||||
// layouts
|
||||
func (builder *MultiLayoutBuilder) Build(db chronograf.LayoutsStore) (*layouts.MultiLayoutsStore, error) {
|
||||
func (builder *MultiLayoutBuilder) Build(db chronograf.LayoutsStore) (*multistore.Layouts, error) {
|
||||
// These apps are those handled from a directory
|
||||
apps := canned.NewApps(builder.CannedPath, builder.UUID, builder.Logger)
|
||||
apps := filestore.NewApps(builder.CannedPath, builder.UUID, builder.Logger)
|
||||
// These apps are statically compiled into chronograf
|
||||
binApps := &canned.BinLayoutsStore{
|
||||
Logger: builder.Logger,
|
||||
|
@ -31,7 +32,7 @@ func (builder *MultiLayoutBuilder) Build(db chronograf.LayoutsStore) (*layouts.M
|
|||
// Acts as a front-end to both the bolt layouts, filesystem layouts and binary statically compiled layouts.
|
||||
// The idea here is that these stores form a hierarchy in which each is tried sequentially until
|
||||
// the operation has success. So, the database is preferred over filesystem over binary data.
|
||||
layouts := &layouts.MultiLayoutsStore{
|
||||
layouts := &multistore.Layouts{
|
||||
Stores: []chronograf.LayoutsStore{
|
||||
db,
|
||||
apps,
|
||||
|
@ -42,9 +43,38 @@ func (builder *MultiLayoutBuilder) Build(db chronograf.LayoutsStore) (*layouts.M
|
|||
return layouts, nil
|
||||
}
|
||||
|
||||
// DashboardBuilder is responsible for building dashboards
|
||||
type DashboardBuilder interface {
|
||||
Build(chronograf.DashboardsStore) (*multistore.DashboardsStore, error)
|
||||
}
|
||||
|
||||
// MultiDashboardBuilder builds a DashboardsStore backed by bolt and the filesystem
|
||||
type MultiDashboardBuilder struct {
|
||||
Logger chronograf.Logger
|
||||
ID chronograf.ID
|
||||
Path string
|
||||
}
|
||||
|
||||
// Build will construct a Dashboard store of filesystem and db-backed dashboards
|
||||
func (builder *MultiDashboardBuilder) Build(db chronograf.DashboardsStore) (*multistore.DashboardsStore, error) {
|
||||
// These dashboards are those handled from a directory
|
||||
files := filestore.NewDashboards(builder.Path, builder.ID, builder.Logger)
|
||||
// Acts as a front-end to both the bolt dashboard and filesystem dashboards.
|
||||
// The idea here is that these stores form a hierarchy in which each is tried sequentially until
|
||||
// the operation has success. So, the database is preferred over filesystem
|
||||
dashboards := &multistore.DashboardsStore{
|
||||
Stores: []chronograf.DashboardsStore{
|
||||
db,
|
||||
files,
|
||||
},
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
// SourcesBuilder builds a MultiSourceStore
|
||||
type SourcesBuilder interface {
|
||||
Build(chronograf.SourcesStore) (*memdb.MultiSourcesStore, error)
|
||||
Build(chronograf.SourcesStore) (*multistore.SourcesStore, error)
|
||||
}
|
||||
|
||||
// MultiSourceBuilder implements SourcesBuilder
|
||||
|
@ -52,11 +82,18 @@ type MultiSourceBuilder struct {
|
|||
InfluxDBURL string
|
||||
InfluxDBUsername string
|
||||
InfluxDBPassword string
|
||||
|
||||
Logger chronograf.Logger
|
||||
ID chronograf.ID
|
||||
Path string
|
||||
}
|
||||
|
||||
// Build will return a MultiSourceStore
|
||||
func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*memdb.MultiSourcesStore, error) {
|
||||
stores := []chronograf.SourcesStore{db}
|
||||
func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*multistore.SourcesStore, error) {
|
||||
// These dashboards are those handled from a directory
|
||||
files := filestore.NewSources(fs.Path, fs.ID, fs.Logger)
|
||||
|
||||
stores := []chronograf.SourcesStore{db, files}
|
||||
|
||||
if fs.InfluxDBURL != "" {
|
||||
influxStore := &memdb.SourcesStore{
|
||||
|
@ -71,7 +108,7 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*memdb.MultiSou
|
|||
}}
|
||||
stores = append([]chronograf.SourcesStore{influxStore}, stores...)
|
||||
}
|
||||
sources := &memdb.MultiSourcesStore{
|
||||
sources := &multistore.SourcesStore{
|
||||
Stores: stores,
|
||||
}
|
||||
|
||||
|
@ -80,7 +117,7 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*memdb.MultiSou
|
|||
|
||||
// KapacitorBuilder builds a KapacitorStore
|
||||
type KapacitorBuilder interface {
|
||||
Build(chronograf.ServersStore) (*memdb.MultiKapacitorStore, error)
|
||||
Build(chronograf.ServersStore) (*multistore.KapacitorStore, error)
|
||||
}
|
||||
|
||||
// MultiKapacitorBuilder implements KapacitorBuilder
|
||||
|
@ -88,11 +125,19 @@ type MultiKapacitorBuilder struct {
|
|||
KapacitorURL string
|
||||
KapacitorUsername string
|
||||
KapacitorPassword string
|
||||
|
||||
Logger chronograf.Logger
|
||||
ID chronograf.ID
|
||||
Path string
|
||||
}
|
||||
|
||||
// Build will return a MultiKapacitorStore
|
||||
func (builder *MultiKapacitorBuilder) Build(db chronograf.ServersStore) (*memdb.MultiKapacitorStore, error) {
|
||||
stores := []chronograf.ServersStore{db}
|
||||
// Build will return a multistore facade KapacitorStore over memdb and bolt
|
||||
func (builder *MultiKapacitorBuilder) Build(db chronograf.ServersStore) (*multistore.KapacitorStore, error) {
|
||||
// These dashboards are those handled from a directory
|
||||
files := filestore.NewKapacitors(builder.Path, builder.ID, builder.Logger)
|
||||
|
||||
stores := []chronograf.ServersStore{db, files}
|
||||
|
||||
if builder.KapacitorURL != "" {
|
||||
memStore := &memdb.KapacitorStore{
|
||||
Kapacitor: &chronograf.Server{
|
||||
|
@ -106,8 +151,36 @@ func (builder *MultiKapacitorBuilder) Build(db chronograf.ServersStore) (*memdb.
|
|||
}
|
||||
stores = append([]chronograf.ServersStore{memStore}, stores...)
|
||||
}
|
||||
kapacitors := &memdb.MultiKapacitorStore{
|
||||
kapacitors := &multistore.KapacitorStore{
|
||||
Stores: stores,
|
||||
}
|
||||
return kapacitors, nil
|
||||
}
|
||||
|
||||
// OrganizationBuilder is responsible for building dashboards
|
||||
type OrganizationBuilder interface {
|
||||
Build(chronograf.OrganizationsStore) (*multistore.OrganizationsStore, error)
|
||||
}
|
||||
|
||||
// MultiOrganizationBuilder builds a OrganizationsStore backed by bolt and the filesystem
|
||||
type MultiOrganizationBuilder struct {
|
||||
Logger chronograf.Logger
|
||||
Path string
|
||||
}
|
||||
|
||||
// Build will construct a Organization store of filesystem and db-backed dashboards
|
||||
func (builder *MultiOrganizationBuilder) Build(db chronograf.OrganizationsStore) (*multistore.OrganizationsStore, error) {
|
||||
// These organization are those handled from a directory
|
||||
files := filestore.NewOrganizations(builder.Path, builder.Logger)
|
||||
// Acts as a front-end to both the bolt org and filesystem orgs.
|
||||
// The idea here is that these stores form a hierarchy in which each is tried sequentially until
|
||||
// the operation has success. So, the database is preferred over filesystem
|
||||
orgs := &multistore.OrganizationsStore{
|
||||
Stores: []chronograf.OrganizationsStore{
|
||||
db,
|
||||
files,
|
||||
},
|
||||
}
|
||||
|
||||
return orgs, nil
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/uuid"
|
||||
idgen "github.com/influxdata/chronograf/id"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -26,45 +26,49 @@ type dashboardCellResponse struct {
|
|||
Links dashboardCellLinks `json:"links"`
|
||||
}
|
||||
|
||||
func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardCell) []dashboardCellResponse {
|
||||
func newCellResponse(dID chronograf.DashboardID, cell chronograf.DashboardCell) dashboardCellResponse {
|
||||
base := "/chronograf/v1/dashboards"
|
||||
newCell := chronograf.DashboardCell{}
|
||||
newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries))
|
||||
copy(newCell.Queries, cell.Queries)
|
||||
|
||||
newCell.CellColors = make([]chronograf.CellColor, len(cell.CellColors))
|
||||
copy(newCell.CellColors, cell.CellColors)
|
||||
|
||||
// ensure x, y, and y2 axes always returned
|
||||
labels := []string{"x", "y", "y2"}
|
||||
newCell.Axes = make(map[string]chronograf.Axis, len(labels))
|
||||
|
||||
newCell.X = cell.X
|
||||
newCell.Y = cell.Y
|
||||
newCell.W = cell.W
|
||||
newCell.H = cell.H
|
||||
newCell.Name = cell.Name
|
||||
newCell.ID = cell.ID
|
||||
newCell.Type = cell.Type
|
||||
|
||||
for _, lbl := range labels {
|
||||
if axis, found := cell.Axes[lbl]; !found {
|
||||
newCell.Axes[lbl] = chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
}
|
||||
} else {
|
||||
newCell.Axes[lbl] = axis
|
||||
}
|
||||
}
|
||||
|
||||
return dashboardCellResponse{
|
||||
DashboardCell: newCell,
|
||||
Links: dashboardCellLinks{
|
||||
Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardCell) []dashboardCellResponse {
|
||||
cells := make([]dashboardCellResponse, len(dcells))
|
||||
for i, cell := range dcells {
|
||||
newCell := chronograf.DashboardCell{}
|
||||
newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries))
|
||||
copy(newCell.Queries, cell.Queries)
|
||||
|
||||
newCell.CellColors = make([]chronograf.CellColor, len(cell.CellColors))
|
||||
copy(newCell.CellColors, cell.CellColors)
|
||||
|
||||
// ensure x, y, and y2 axes always returned
|
||||
labels := []string{"x", "y", "y2"}
|
||||
newCell.Axes = make(map[string]chronograf.Axis, len(labels))
|
||||
|
||||
newCell.X = cell.X
|
||||
newCell.Y = cell.Y
|
||||
newCell.W = cell.W
|
||||
newCell.H = cell.H
|
||||
newCell.Name = cell.Name
|
||||
newCell.ID = cell.ID
|
||||
newCell.Type = cell.Type
|
||||
|
||||
for _, lbl := range labels {
|
||||
if axis, found := cell.Axes[lbl]; !found {
|
||||
newCell.Axes[lbl] = chronograf.Axis{
|
||||
Bounds: []string{},
|
||||
}
|
||||
} else {
|
||||
newCell.Axes[lbl] = axis
|
||||
}
|
||||
}
|
||||
|
||||
cells[i] = dashboardCellResponse{
|
||||
DashboardCell: newCell,
|
||||
Links: dashboardCellLinks{
|
||||
Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID),
|
||||
},
|
||||
}
|
||||
cells[i] = newCellResponse(dID, cell)
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
@ -112,7 +116,7 @@ func HasCorrectAxes(c *chronograf.DashboardCell) error {
|
|||
// HasCorrectColors verifies that the format of each color is correct
|
||||
func HasCorrectColors(c *chronograf.DashboardCell) error {
|
||||
for _, color := range c.CellColors {
|
||||
if !oneOf(color.Type, "max", "min", "threshold") {
|
||||
if !oneOf(color.Type, "max", "min", "threshold", "text", "background") {
|
||||
return chronograf.ErrInvalidColorType
|
||||
}
|
||||
if len(color.Hex) != 7 {
|
||||
|
@ -210,7 +214,7 @@ func (s *Service) NewDashboardCell(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
ids := uuid.V4{}
|
||||
ids := &idgen.UUID{}
|
||||
cid, err := ids.Generate()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Error creating cell ID of dashboard %d: %v", id, err)
|
||||
|
@ -322,7 +326,7 @@ func (s *Service) ReplaceDashboardCell(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
if cellid == -1 {
|
||||
notFound(w, id, s.Logger)
|
||||
notFound(w, cid, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -345,11 +349,6 @@ func (s *Service) ReplaceDashboardCell(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
boards := newDashboardResponse(dash)
|
||||
for _, cell := range boards.Cells {
|
||||
if cell.ID == cid {
|
||||
encodeJSON(w, http.StatusOK, cell, s.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
res := newCellResponse(dash.ID, cell)
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package server_test
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
@ -13,7 +16,6 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
"github.com/influxdata/chronograf/server"
|
||||
)
|
||||
|
||||
func Test_Cells_CorrectAxis(t *testing.T) {
|
||||
|
@ -126,7 +128,7 @@ func Test_Cells_CorrectAxis(t *testing.T) {
|
|||
|
||||
for _, test := range axisTests {
|
||||
t.Run(test.name, func(tt *testing.T) {
|
||||
if err := server.HasCorrectAxes(test.cell); err != nil && !test.shouldFail {
|
||||
if err := HasCorrectAxes(test.cell); err != nil && !test.shouldFail {
|
||||
t.Errorf("%q: Unexpected error: err: %s", test.name, err)
|
||||
} else if err == nil && test.shouldFail {
|
||||
t.Errorf("%q: Expected error and received none", test.name)
|
||||
|
@ -226,7 +228,7 @@ func Test_Service_DashboardCells(t *testing.T) {
|
|||
|
||||
// setup mock DashboardCells store and logger
|
||||
tlog := &mocks.TestLogger{}
|
||||
svc := &server.Service{
|
||||
svc := &Service{
|
||||
Store: &mocks.Store{
|
||||
DashboardsStore: &mocks.DashboardsStore{
|
||||
GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
|
@ -343,9 +345,490 @@ func TestHasCorrectColors(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := server.HasCorrectColors(tt.c); (err != nil) != tt.wantErr {
|
||||
if err := HasCorrectColors(tt.c); (err != nil) != tt.wantErr {
|
||||
t.Errorf("HasCorrectColors() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ReplaceDashboardCell(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
DashboardsStore chronograf.DashboardsStore
|
||||
ID string
|
||||
CID string
|
||||
w *httptest.ResponseRecorder
|
||||
r *http.Request
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "update cell retains query config",
|
||||
ID: "1",
|
||||
CID: "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
DashboardsStore: &mocks.DashboardsStore{
|
||||
UpdateF: func(ctx context.Context, target chronograf.Dashboard) error {
|
||||
return nil
|
||||
},
|
||||
GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
return chronograf.Dashboard{
|
||||
ID: ID,
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Untitled Cell",
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)",
|
||||
QueryConfig: chronograf.QueryConfig{
|
||||
ID: "3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e",
|
||||
Database: "telegraf",
|
||||
Measurement: "cpu",
|
||||
RetentionPolicy: "autogen",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Value: "mean",
|
||||
Type: "func",
|
||||
Alias: "mean_usage_user",
|
||||
Args: []chronograf.Field{
|
||||
{
|
||||
Value: "usage_user",
|
||||
Type: "field",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Tags: map[string][]string{
|
||||
"cpu": {
|
||||
"ChristohersMBP2.lan",
|
||||
},
|
||||
},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Time: "2s",
|
||||
Tags: []string{},
|
||||
},
|
||||
AreTagsAccepted: true,
|
||||
Fill: "null",
|
||||
RawText: strPtr("SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)"),
|
||||
Range: &chronograf.DurationRange{
|
||||
Lower: "now() - 15m"},
|
||||
Shifts: []chronograf.TimeShift{},
|
||||
},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": {
|
||||
Bounds: []string{},
|
||||
},
|
||||
"y": {
|
||||
Bounds: []string{},
|
||||
},
|
||||
"y2": {
|
||||
Bounds: []string{},
|
||||
},
|
||||
},
|
||||
Type: "line",
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
ID: "0",
|
||||
Type: "min",
|
||||
Hex: "#00C9FF",
|
||||
Name: "laser",
|
||||
Value: "0",
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Type: "max",
|
||||
Hex: "#9394FF",
|
||||
Name: "comet",
|
||||
Value: "100",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`
|
||||
{
|
||||
"i": "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 4,
|
||||
"h": 4,
|
||||
"name": "Untitled Cell",
|
||||
"queries": [
|
||||
{
|
||||
"queryConfig": {
|
||||
"id": "3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e",
|
||||
"database": "telegraf",
|
||||
"measurement": "cpu",
|
||||
"retentionPolicy": "autogen",
|
||||
"fields": [
|
||||
{
|
||||
"value": "mean",
|
||||
"type": "func",
|
||||
"alias": "mean_usage_user",
|
||||
"args": [{"value": "usage_user", "type": "field", "alias": ""}]
|
||||
}
|
||||
],
|
||||
"tags": {"cpu": ["ChristohersMBP2.lan"]},
|
||||
"groupBy": {"time": "2s", "tags": []},
|
||||
"areTagsAccepted": true,
|
||||
"fill": "null",
|
||||
"rawText":
|
||||
"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)",
|
||||
"range": {"upper": "", "lower": "now() - 15m"},
|
||||
"shifts": []
|
||||
},
|
||||
"query":
|
||||
"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)",
|
||||
"source": null
|
||||
}
|
||||
],
|
||||
"axes": {
|
||||
"x": {
|
||||
"bounds": [],
|
||||
"label": "",
|
||||
"prefix": "",
|
||||
"suffix": "",
|
||||
"base": "",
|
||||
"scale": ""
|
||||
},
|
||||
"y": {
|
||||
"bounds": [],
|
||||
"label": "",
|
||||
"prefix": "",
|
||||
"suffix": "",
|
||||
"base": "",
|
||||
"scale": ""
|
||||
},
|
||||
"y2": {
|
||||
"bounds": [],
|
||||
"label": "",
|
||||
"prefix": "",
|
||||
"suffix": "",
|
||||
"base": "",
|
||||
"scale": ""
|
||||
}
|
||||
},
|
||||
"type": "line",
|
||||
"colors": [
|
||||
{"type": "min", "hex": "#00C9FF", "id": "0", "name": "laser", "value": "0"},
|
||||
{
|
||||
"type": "max",
|
||||
"hex": "#9394FF",
|
||||
"id": "1",
|
||||
"name": "comet",
|
||||
"value": "100"
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self":
|
||||
"/chronograf/v1/dashboards/6/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"
|
||||
}
|
||||
}
|
||||
`))),
|
||||
want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":[],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "dashboard doesn't exist",
|
||||
ID: "1",
|
||||
DashboardsStore: &mocks.DashboardsStore{
|
||||
GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
return chronograf.Dashboard{}, fmt.Errorf("doesn't exist")
|
||||
},
|
||||
},
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", nil),
|
||||
want: `{"code":404,"message":"ID 1 not found"}`,
|
||||
},
|
||||
{
|
||||
name: "cell doesn't exist",
|
||||
ID: "1",
|
||||
CID: "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
DashboardsStore: &mocks.DashboardsStore{
|
||||
GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
return chronograf.Dashboard{}, nil
|
||||
},
|
||||
},
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", nil),
|
||||
want: `{"code":404,"message":"ID 3c5c4102-fa40-4585-a8f9-917c77e37192 not found"}`,
|
||||
},
|
||||
{
|
||||
name: "invalid query config",
|
||||
ID: "1",
|
||||
CID: "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
DashboardsStore: &mocks.DashboardsStore{
|
||||
GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
return chronograf.Dashboard{
|
||||
ID: ID,
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", bytes.NewReader([]byte(`{
|
||||
"i": "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 4,
|
||||
"h": 4,
|
||||
"name": "Untitled Cell",
|
||||
"queries": [
|
||||
{
|
||||
"queryConfig": {
|
||||
"fields": [
|
||||
{
|
||||
"value": "invalid",
|
||||
"type": "invalidType"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`))),
|
||||
want: `{"code":422,"message":"invalid field type \"invalidType\" ; expect func, field, integer, number, regex, wildcard"}`,
|
||||
},
|
||||
{
|
||||
name: "JSON is not parsable",
|
||||
ID: "1",
|
||||
CID: "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
DashboardsStore: &mocks.DashboardsStore{
|
||||
GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
return chronograf.Dashboard{
|
||||
ID: ID,
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", nil),
|
||||
want: `{"code":400,"message":"Unparsable JSON"}`,
|
||||
},
|
||||
{
|
||||
name: "not able to update store returns error message",
|
||||
ID: "1",
|
||||
CID: "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
DashboardsStore: &mocks.DashboardsStore{
|
||||
UpdateF: func(ctx context.Context, target chronograf.Dashboard) error {
|
||||
return fmt.Errorf("error")
|
||||
},
|
||||
GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) {
|
||||
return chronograf.Dashboard{
|
||||
ID: ID,
|
||||
Cells: []chronograf.DashboardCell{
|
||||
{
|
||||
ID: "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", bytes.NewReader([]byte(`{
|
||||
"i": "3c5c4102-fa40-4585-a8f9-917c77e37192",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 4,
|
||||
"h": 4,
|
||||
"name": "Untitled Cell",
|
||||
"queries": [
|
||||
{
|
||||
"queryConfig": {
|
||||
"fields": [
|
||||
{
|
||||
"value": "usage_user",
|
||||
"type": "field"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`))),
|
||||
want: `{"code":500,"message":"Error updating cell 3c5c4102-fa40-4585-a8f9-917c77e37192 in dashboard 1: error"}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
DashboardsStore: tt.DashboardsStore,
|
||||
},
|
||||
Logger: &mocks.TestLogger{},
|
||||
}
|
||||
tt.r = WithContext(tt.r.Context(), tt.r, map[string]string{
|
||||
"id": tt.ID,
|
||||
"cid": tt.CID,
|
||||
})
|
||||
s.ReplaceDashboardCell(tt.w, tt.r)
|
||||
got := tt.w.Body.String()
|
||||
if got != tt.want {
|
||||
t.Errorf("ReplaceDashboardCell() = got/want\n%s\n%s\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func Test_newCellResponses(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dID chronograf.DashboardID
|
||||
dcells []chronograf.DashboardCell
|
||||
want []dashboardCellResponse
|
||||
}{
|
||||
{
|
||||
name: "foo",
|
||||
dID: chronograf.DashboardID(1),
|
||||
dcells: []chronograf.DashboardCell{
|
||||
chronograf.DashboardCell{
|
||||
ID: "445f8dc0-4d73-4168-8477-f628690d18a3",
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Untitled Cell",
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)",
|
||||
Label: "",
|
||||
QueryConfig: chronograf.QueryConfig{
|
||||
ID: "8d5ec6da-13a5-423e-9026-7bc45649766c",
|
||||
Database: "telegraf",
|
||||
Measurement: "cpu",
|
||||
RetentionPolicy: "autogen",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Value: "mean",
|
||||
Type: "func",
|
||||
Alias: "mean_usage_user",
|
||||
Args: []chronograf.Field{
|
||||
{
|
||||
Value: "usage_user",
|
||||
Type: "field",
|
||||
Alias: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Tags: map[string][]string{"cpu": []string{"ChristohersMBP2.lan"}},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Time: "2s",
|
||||
},
|
||||
AreTagsAccepted: true,
|
||||
Fill: "null",
|
||||
RawText: strPtr("SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)"),
|
||||
Range: &chronograf.DurationRange{
|
||||
Lower: "now() - 15m",
|
||||
},
|
||||
},
|
||||
Source: "",
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": chronograf.Axis{},
|
||||
"y": chronograf.Axis{},
|
||||
"y2": chronograf.Axis{},
|
||||
},
|
||||
Type: "line",
|
||||
CellColors: []chronograf.CellColor{
|
||||
chronograf.CellColor{ID: "0", Type: "min", Hex: "#00C9FF", Name: "laser", Value: "0"},
|
||||
chronograf.CellColor{ID: "1", Type: "max", Hex: "#9394FF", Name: "comet", Value: "100"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []dashboardCellResponse{
|
||||
{
|
||||
DashboardCell: chronograf.DashboardCell{
|
||||
ID: "445f8dc0-4d73-4168-8477-f628690d18a3",
|
||||
W: 4,
|
||||
H: 4,
|
||||
Name: "Untitled Cell",
|
||||
Queries: []chronograf.DashboardQuery{
|
||||
{
|
||||
Command: "SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)",
|
||||
QueryConfig: chronograf.QueryConfig{
|
||||
ID: "8d5ec6da-13a5-423e-9026-7bc45649766c",
|
||||
Database: "telegraf",
|
||||
Measurement: "cpu",
|
||||
RetentionPolicy: "autogen",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Value: "mean",
|
||||
Type: "func",
|
||||
Alias: "mean_usage_user",
|
||||
Args: []chronograf.Field{
|
||||
{
|
||||
Value: "usage_user",
|
||||
Type: "field",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Tags: map[string][]string{"cpu": {"ChristohersMBP2.lan"}},
|
||||
GroupBy: chronograf.GroupBy{
|
||||
Time: "2s",
|
||||
},
|
||||
AreTagsAccepted: true,
|
||||
Fill: "null",
|
||||
RawText: strPtr("SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)"),
|
||||
Range: &chronograf.DurationRange{
|
||||
Lower: "now() - 15m",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Axes: map[string]chronograf.Axis{
|
||||
"x": {},
|
||||
"y": {},
|
||||
"y2": {},
|
||||
},
|
||||
Type: "line",
|
||||
CellColors: []chronograf.CellColor{
|
||||
{
|
||||
ID: "0",
|
||||
Type: "min",
|
||||
Hex: "#00C9FF",
|
||||
Name: "laser",
|
||||
Value: "0",
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Type: "max",
|
||||
Hex: "#9394FF",
|
||||
Name: "comet",
|
||||
Value: "100",
|
||||
},
|
||||
},
|
||||
},
|
||||
Links: dashboardCellLinks{
|
||||
Self: "/chronograf/v1/dashboards/1/cells/445f8dc0-4d73-4168-8477-f628690d18a3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := newCellResponses(tt.dID, tt.dcells); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("newCellResponses() = got-/want+ %s", cmp.Diff(got, tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
type configResponse struct {
|
||||
Links selfLinks `json:"links"`
|
||||
chronograf.Config
|
||||
}
|
||||
|
||||
func newConfigResponse(config chronograf.Config) *configResponse {
|
||||
return &configResponse{
|
||||
Links: selfLinks{
|
||||
Self: "/chronograf/v1/config",
|
||||
},
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type authConfigResponse struct {
|
||||
Links selfLinks `json:"links"`
|
||||
chronograf.AuthConfig
|
||||
}
|
||||
|
||||
func newAuthConfigResponse(config chronograf.Config) *authConfigResponse {
|
||||
return &authConfigResponse{
|
||||
Links: selfLinks{
|
||||
Self: "/chronograf/v1/config/auth",
|
||||
},
|
||||
AuthConfig: config.Auth,
|
||||
}
|
||||
}
|
||||
|
||||
// Config retrieves the global application configuration
|
||||
func (s *Service) Config(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
config, err := s.Store.Config(ctx).Get(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger)
|
||||
return
|
||||
}
|
||||
res := newConfigResponse(*config)
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// ConfigSection retrieves the section of the global application configuration
|
||||
func (s *Service) ConfigSection(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
config, err := s.Store.Config(ctx).Get(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
section := httprouter.GetParamFromContext(ctx, "section")
|
||||
var res interface{}
|
||||
switch section {
|
||||
case "auth":
|
||||
res = newAuthConfigResponse(*config)
|
||||
default:
|
||||
Error(w, http.StatusBadRequest, fmt.Sprintf("received unknown section %q", section), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// ReplaceConfigSection replaces a section of the global application configuration
|
||||
func (s *Service) ReplaceConfigSection(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
config, err := s.Store.Config(ctx).Get(ctx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
section := httprouter.GetParamFromContext(ctx, "section")
|
||||
var res interface{}
|
||||
switch section {
|
||||
case "auth":
|
||||
var authConfig chronograf.AuthConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&authConfig); err != nil {
|
||||
invalidJSON(w, s.Logger)
|
||||
return
|
||||
}
|
||||
config.Auth = authConfig
|
||||
res = newAuthConfigResponse(*config)
|
||||
default:
|
||||
Error(w, http.StatusBadRequest, fmt.Sprintf("received unknown section %q", section), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Store.Config(ctx).Update(ctx, config); err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
type fields struct {
|
||||
ConfigStore chronograf.ConfigStore
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "Get global application configuration",
|
||||
fields: fields{
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
contentType: "application/json",
|
||||
body: `{"auth": {"superAdminNewUsers": false}, "links": {"self": "/chronograf/v1/config"}}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
|
||||
s.Config(w, r)
|
||||
|
||||
resp := w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSection(t *testing.T) {
|
||||
type fields struct {
|
||||
ConfigStore chronograf.ConfigStore
|
||||
}
|
||||
type args struct {
|
||||
section string
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "Get auth configuration",
|
||||
fields: fields{
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
section: "auth",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
contentType: "application/json",
|
||||
body: `{"superAdminNewUsers": false, "links": {"self": "/chronograf/v1/config/auth"}}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Get unknown configuration",
|
||||
fields: fields{
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
section: "unknown",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 400,
|
||||
contentType: "application/json",
|
||||
body: `{"code":400,"message":"received unknown section \"unknown\""}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
r = r.WithContext(httprouter.WithParams(
|
||||
r.Context(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "section",
|
||||
Value: tt.args.section,
|
||||
},
|
||||
}))
|
||||
|
||||
s.ConfigSection(w, r)
|
||||
|
||||
resp := w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceConfigSection(t *testing.T) {
|
||||
type fields struct {
|
||||
ConfigStore chronograf.ConfigStore
|
||||
}
|
||||
type args struct {
|
||||
section string
|
||||
payload interface{} // expects JSON serializable struct
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "Set auth configuration",
|
||||
fields: fields{
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
section: "auth",
|
||||
payload: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
contentType: "application/json",
|
||||
body: `{"superAdminNewUsers": true, "links": {"self": "/chronograf/v1/config/auth"}}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Set unknown configuration",
|
||||
fields: fields{
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
section: "unknown",
|
||||
payload: struct {
|
||||
Data string `json:"data"`
|
||||
}{
|
||||
Data: "stuff",
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 400,
|
||||
contentType: "application/json",
|
||||
body: `{"code":400,"message":"received unknown section \"unknown\""}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
r = r.WithContext(httprouter.WithParams(
|
||||
r.Context(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "section",
|
||||
Value: tt.args.section,
|
||||
},
|
||||
}))
|
||||
buf, _ := json.Marshal(tt.args.payload)
|
||||
r.Body = ioutil.NopCloser(bytes.NewReader(buf))
|
||||
|
||||
s.ReplaceConfigSection(w, r)
|
||||
|
||||
resp := w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -101,7 +101,7 @@ func (s *Service) NewDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := ValidDashboardRequest(&dashboard, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
|
||||
if err := ValidDashboardRequest(&dashboard, defaultOrg.ID); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := ValidDashboardRequest(&req, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
|
||||
if err := ValidDashboardRequest(&req, defaultOrg.ID); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -215,7 +215,7 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
if err := ValidDashboardRequest(&req, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
|
||||
if err := ValidDashboardRequest(&req, defaultOrg.ID); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
type envResponse struct {
|
||||
Links selfLinks `json:"links"`
|
||||
TelegrafSystemInterval string `json:"telegrafSystemInterval"`
|
||||
}
|
||||
|
||||
func newEnvResponse(env chronograf.Environment) *envResponse {
|
||||
return &envResponse{
|
||||
Links: selfLinks{
|
||||
Self: "/chronograf/v1/env",
|
||||
},
|
||||
TelegrafSystemInterval: env.TelegrafSystemInterval.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// Environment retrieves the global application configuration
|
||||
func (s *Service) Environment(w http.ResponseWriter, r *http.Request) {
|
||||
res := newEnvResponse(s.Env)
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
)
|
||||
|
||||
func TestEnvironment(t *testing.T) {
|
||||
type fields struct {
|
||||
Environment chronograf.Environment
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "Get environment",
|
||||
fields: fields{
|
||||
Environment: chronograf.Environment{
|
||||
TelegrafSystemInterval: 1 * time.Minute,
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: 200,
|
||||
contentType: "application/json",
|
||||
body: `{"links":{"self":"/chronograf/v1/env"},"telegrafSystemInterval":"1m0s"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Env: tt.fields.Environment,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
|
||||
s.Environment(w, r)
|
||||
|
||||
resp := w.Result()
|
||||
content := resp.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -87,7 +87,7 @@ func (s *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := req.Valid(fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
|
||||
if err := req.Valid(defaultOrg.ID); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -5,6 +5,11 @@ import (
|
|||
"net/url"
|
||||
)
|
||||
|
||||
type getConfigLinksResponse struct {
|
||||
Self string `json:"self"` // Location of the whole global application configuration
|
||||
Auth string `json:"auth"` // Location of the auth section of the global application configuration
|
||||
}
|
||||
|
||||
type getExternalLinksResponse struct {
|
||||
StatusFeed *string `json:"statusFeed,omitempty"` // Location of the a JSON Feed for client's Status page News Feed
|
||||
CustomLinks []CustomLink `json:"custom,omitempty"` // Any custom external links for client's User menu
|
||||
|
|
108
server/me.go
108
server/me.go
|
@ -95,12 +95,7 @@ func (s *Service) UpdateMe(auth oauth2.Authenticator) func(http.ResponseWriter,
|
|||
}
|
||||
|
||||
// validate that the organization exists
|
||||
orgID, err := parseOrganizationID(req.Organization)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
_, err = s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &orgID})
|
||||
org, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &req.Organization})
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
|
@ -120,7 +115,7 @@ func (s *Service) UpdateMe(auth oauth2.Authenticator) func(http.ResponseWriter,
|
|||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
p.Organization = fmt.Sprintf("%d", defaultOrg.ID)
|
||||
p.Organization = defaultOrg.ID
|
||||
}
|
||||
scheme, err := getScheme(ctx)
|
||||
if err != nil {
|
||||
|
@ -133,11 +128,36 @@ func (s *Service) UpdateMe(auth oauth2.Authenticator) func(http.ResponseWriter,
|
|||
Scheme: &scheme,
|
||||
})
|
||||
if err == chronograf.ErrUserNotFound {
|
||||
// Since a user is not a part of this organization, we should tell them that they are Forbidden (403) from accessing this resource
|
||||
Error(w, http.StatusForbidden, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
// If the user was not found, check to see if they are a super admin. If
|
||||
// they are, add them to the organization.
|
||||
u, err := s.Store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{
|
||||
Name: &p.Subject,
|
||||
Provider: &p.Issuer,
|
||||
Scheme: &scheme,
|
||||
})
|
||||
if err != nil {
|
||||
Error(w, http.StatusForbidden, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if u.SuperAdmin == false {
|
||||
// Since a user is not a part of this organization and not a super admin,
|
||||
// we should tell them that they are Forbidden (403) from accessing this resource
|
||||
Error(w, http.StatusForbidden, chronograf.ErrUserNotFound.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// If the user is a super admin give them an admin role in the
|
||||
// requested organization.
|
||||
u.Roles = append(u.Roles, chronograf.Role{
|
||||
Organization: org.ID,
|
||||
Name: org.DefaultRole,
|
||||
})
|
||||
if err := s.Store.Users(serverCtx).Update(serverCtx, u); err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -186,7 +206,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
p.Organization = fmt.Sprintf("%d", defaultOrg.ID)
|
||||
p.Organization = defaultOrg.ID
|
||||
}
|
||||
|
||||
usr, err := s.Store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{
|
||||
|
@ -206,17 +226,29 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if usr != nil {
|
||||
|
||||
if defaultOrg.Public || usr.SuperAdmin == true {
|
||||
// If the default organization is public, or the user is a super admin
|
||||
// they will always have a role in the default organization
|
||||
defaultOrgID := defaultOrg.ID
|
||||
if !hasRoleInDefaultOrganization(usr, defaultOrgID) {
|
||||
usr.Roles = append(usr.Roles, chronograf.Role{
|
||||
Organization: defaultOrgID,
|
||||
Name: defaultOrg.DefaultRole,
|
||||
})
|
||||
if err := s.Store.Users(serverCtx).Update(serverCtx, usr); err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the default org is private and the user has no roles, they should not have access
|
||||
if !defaultOrg.Public && len(usr.Roles) == 0 {
|
||||
Error(w, http.StatusForbidden, "This organization is private. To gain access, you must be explicitly added by an administrator.", s.Logger)
|
||||
return
|
||||
}
|
||||
orgID, err := parseOrganizationID(p.Organization)
|
||||
if err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &orgID})
|
||||
currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization})
|
||||
if err == chronograf.ErrOrganizationNotFound {
|
||||
// The intent is to force a the user to go through another auth flow
|
||||
Error(w, http.StatusForbidden, "user's current organization was not found", s.Logger)
|
||||
|
@ -227,19 +259,6 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
defaultOrgID := fmt.Sprintf("%d", defaultOrg.ID)
|
||||
// If a user was added via the API, they might not yet be a member of the default organization
|
||||
// Here we check to verify that they are a user in the default organization
|
||||
if !hasRoleInDefaultOrganization(usr, defaultOrgID) {
|
||||
usr.Roles = append(usr.Roles, chronograf.Role{
|
||||
Organization: defaultOrgID,
|
||||
Name: defaultOrg.DefaultRole,
|
||||
})
|
||||
if err := s.Store.Users(serverCtx).Update(serverCtx, usr); err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
orgs, err := s.usersOrganizations(serverCtx, usr)
|
||||
if err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
|
@ -271,7 +290,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
{
|
||||
Name: defaultOrg.DefaultRole,
|
||||
// This is the ID of the default organization
|
||||
Organization: fmt.Sprintf("%d", defaultOrg.ID),
|
||||
Organization: defaultOrg.ID,
|
||||
},
|
||||
},
|
||||
// TODO(desa): this needs a better name
|
||||
|
@ -290,12 +309,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
|
|||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
orgID, err := parseOrganizationID(p.Organization)
|
||||
if err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &orgID})
|
||||
currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization})
|
||||
if err != nil {
|
||||
unknownErrorWithMessage(w, err, s.Logger)
|
||||
return
|
||||
|
@ -316,10 +330,21 @@ func (s *Service) firstUser() bool {
|
|||
return numUsers == 0
|
||||
}
|
||||
func (s *Service) newUsersAreSuperAdmin() bool {
|
||||
// It's not necessary to enforce that the first user is superAdmin here, since
|
||||
// superAdminNewUsers defaults to true, but there's nothing else in the
|
||||
// application that dictates that it must be true.
|
||||
// So for that reason, we kept this here for now. We've discussed the
|
||||
// future possibility of allowing users to override default values via CLI and
|
||||
// this case could possibly happen then.
|
||||
if s.firstUser() {
|
||||
return true
|
||||
}
|
||||
return !s.SuperAdminFirstUserOnly
|
||||
serverCtx := serverContext(context.Background())
|
||||
cfg, err := s.Store.Config(serverCtx).Get(serverCtx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return cfg.Auth.SuperAdminNewUsers
|
||||
}
|
||||
|
||||
func (s *Service) usersOrganizations(ctx context.Context, u *chronograf.User) ([]chronograf.Organization, error) {
|
||||
|
@ -335,8 +360,7 @@ func (s *Service) usersOrganizations(ctx context.Context, u *chronograf.User) ([
|
|||
|
||||
orgs := []chronograf.Organization{}
|
||||
for orgID, _ := range orgIDs {
|
||||
id, err := parseOrganizationID(orgID)
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &orgID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -21,11 +21,11 @@ type MockUsers struct{}
|
|||
|
||||
func TestService_Me(t *testing.T) {
|
||||
type fields struct {
|
||||
UsersStore chronograf.UsersStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
SuperAdminFirstUserOnly bool
|
||||
UsersStore chronograf.UsersStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
|
@ -47,13 +47,19 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
|
@ -61,16 +67,16 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case 0:
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
case 1:
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: 1,
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
Public: false,
|
||||
}, nil
|
||||
|
@ -107,7 +113,7 @@ func TestService_Me(t *testing.T) {
|
|||
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user",
|
||||
name: "Existing user - private default org and user is a super admin",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
|
@ -118,7 +124,138 @@ func TestService_Me(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
NumF: func(ctx context.Context) (int, error) {
|
||||
// This function gets to verify that there is at least one first user
|
||||
return 1, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
|
||||
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
|
||||
}
|
||||
return &chronograf.User{
|
||||
Name: "me",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
}, nil
|
||||
},
|
||||
UpdateF: func(ctx context.Context, u *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "me",
|
||||
Issuer: "github",
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - private default org",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
NumF: func(ctx context.Context) (int, error) {
|
||||
// This function gets to verify that there is at least one first user
|
||||
return 1, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
|
||||
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
|
||||
}
|
||||
return &chronograf.User{
|
||||
Name: "me",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
}, nil
|
||||
},
|
||||
UpdateF: func(ctx context.Context, u *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
principal: oauth2.Principal{
|
||||
Subject: "me",
|
||||
Issuer: "github",
|
||||
},
|
||||
wantStatus: http.StatusForbidden,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - default org public",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -126,16 +263,16 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case 0:
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case 1:
|
||||
case "1":
|
||||
return &chronograf.Organization{
|
||||
ID: 1,
|
||||
ID: "1",
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
}, nil
|
||||
|
@ -169,8 +306,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","defaultRole":"viewer","name":"Default","public":true}}
|
||||
`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Existing user - organization doesn't exist",
|
||||
|
@ -184,7 +320,7 @@ func TestService_Me(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -192,9 +328,9 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case 0:
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -235,13 +371,19 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: false,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -249,7 +391,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -291,13 +433,19 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -305,7 +453,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -347,13 +495,19 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -361,7 +515,7 @@ func TestService_Me(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "The Gnarly Default",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -403,18 +557,24 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
UseAuth: true,
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Public: true,
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "The Bad Place",
|
||||
Public: true,
|
||||
}, nil
|
||||
|
@ -452,9 +612,15 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: false,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: false,
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
|
@ -468,9 +634,15 @@ func TestService_Me(t *testing.T) {
|
|||
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
|
||||
},
|
||||
fields: fields{
|
||||
UseAuth: true,
|
||||
SuperAdminFirstUserOnly: true,
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UseAuth: true,
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
},
|
||||
wantStatus: http.StatusUnprocessableEntity,
|
||||
principal: oauth2.Principal{
|
||||
|
@ -490,7 +662,7 @@ func TestService_Me(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "The Bad Place",
|
||||
DefaultRole: roles.MemberRoleName,
|
||||
Public: false,
|
||||
|
@ -531,10 +703,10 @@ func TestService_Me(t *testing.T) {
|
|||
Store: &mocks.Store{
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
OrganizationsStore: tt.fields.OrganizationsStore,
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
},
|
||||
Logger: tt.fields.Logger,
|
||||
UseAuth: tt.fields.UseAuth,
|
||||
SuperAdminFirstUserOnly: tt.fields.SuperAdminFirstUserOnly,
|
||||
Logger: tt.fields.Logger,
|
||||
UseAuth: tt.fields.UseAuth,
|
||||
}
|
||||
|
||||
s.Me(tt.args.w, tt.args.r)
|
||||
|
@ -617,7 +789,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.AdminRoleName,
|
||||
Public: true,
|
||||
|
@ -628,16 +800,16 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
switch *q.ID {
|
||||
case 0:
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.AdminRoleName,
|
||||
Public: true,
|
||||
}, nil
|
||||
case 1337:
|
||||
case "1337":
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
Public: true,
|
||||
}, nil
|
||||
|
@ -652,7 +824,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","public":true,"defaultRole":"admin"},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"admin","public":true},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
|
||||
},
|
||||
{
|
||||
name: "Change the current User's organization",
|
||||
|
@ -691,7 +863,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
Public: true,
|
||||
|
@ -702,15 +874,15 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
switch *q.ID {
|
||||
case 1337:
|
||||
case "1337":
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ThrillShilliettos",
|
||||
Public: false,
|
||||
}, nil
|
||||
case 0:
|
||||
case "0":
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "Default",
|
||||
DefaultRole: roles.EditorRoleName,
|
||||
Public: true,
|
||||
|
@ -727,7 +899,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","public":true,"defaultRole":"editor"},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
|
||||
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"editor","public":true},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
|
||||
},
|
||||
{
|
||||
name: "Unable to find requested user in valid organization",
|
||||
|
@ -766,7 +938,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
@ -774,7 +946,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
return nil, fmt.Errorf("Invalid organization query: missing ID")
|
||||
}
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The ShillBillThrilliettas",
|
||||
Public: true,
|
||||
}, nil
|
||||
|
@ -827,7 +999,7 @@ func TestService_UpdateMe(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
}, nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
|
|
|
@ -237,6 +237,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.PUT("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", EnsureEditor(service.UpdateRetentionPolicy))
|
||||
router.DELETE("/chronograf/v1/sources/:id/dbs/:dbid/rps/:rpid", EnsureEditor(service.DropRetentionPolicy))
|
||||
|
||||
// Global application config for Chronograf
|
||||
router.GET("/chronograf/v1/config", EnsureSuperAdmin(service.Config))
|
||||
router.GET("/chronograf/v1/config/:section", EnsureSuperAdmin(service.ConfigSection))
|
||||
router.PUT("/chronograf/v1/config/:section", EnsureSuperAdmin(service.ReplaceConfigSection))
|
||||
|
||||
router.GET("/chronograf/v1/env", EnsureViewer(service.Environment))
|
||||
|
||||
allRoutes := &AllRoutes{
|
||||
Logger: opts.Logger,
|
||||
StatusFeed: opts.StatusFeedURL,
|
||||
|
@ -358,8 +365,8 @@ func unknownErrorWithMessage(w http.ResponseWriter, err error, logger chronograf
|
|||
Error(w, http.StatusInternalServerError, fmt.Sprintf("Unknown error: %v", err), logger)
|
||||
}
|
||||
|
||||
func notFound(w http.ResponseWriter, id int, logger chronograf.Logger) {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), logger)
|
||||
func notFound(w http.ResponseWriter, id interface{}, logger chronograf.Logger) {
|
||||
Error(w, http.StatusNotFound, fmt.Sprintf("ID %v not found", id), logger)
|
||||
}
|
||||
|
||||
func paramID(key string, r *http.Request) (int, error) {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
|
@ -13,10 +12,6 @@ import (
|
|||
"github.com/influxdata/chronograf/roles"
|
||||
)
|
||||
|
||||
func parseOrganizationID(id string) (uint64, error) {
|
||||
return strconv.ParseUint(id, 10, 64)
|
||||
}
|
||||
|
||||
type organizationRequest struct {
|
||||
Name string `json:"name"`
|
||||
DefaultRole string `json:"defaultRole"`
|
||||
|
@ -68,7 +63,7 @@ func newOrganizationResponse(o *chronograf.Organization) *organizationResponse {
|
|||
return &organizationResponse{
|
||||
Organization: *o,
|
||||
Links: selfLinks{
|
||||
Self: fmt.Sprintf("/chronograf/v1/organizations/%d", o.ID),
|
||||
Self: fmt.Sprintf("/chronograf/v1/organizations/%s", o.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -144,15 +139,14 @@ func (s *Service) NewOrganization(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
orgID := fmt.Sprintf("%d", res.ID)
|
||||
user.Roles = []chronograf.Role{
|
||||
{
|
||||
Organization: orgID,
|
||||
Organization: res.ID,
|
||||
Name: roles.AdminRoleName,
|
||||
},
|
||||
}
|
||||
|
||||
orgCtx := context.WithValue(ctx, organizations.ContextKey, orgID)
|
||||
orgCtx := context.WithValue(ctx, organizations.ContextKey, res.ID)
|
||||
_, err = s.Store.Users(orgCtx).Add(orgCtx, user)
|
||||
if err != nil {
|
||||
// Best attempt at cleanup the organization if there were any errors adding user to org
|
||||
|
@ -171,12 +165,7 @@ func (s *Service) NewOrganization(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *Service) OrganizationID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
idStr := httprouter.GetParamFromContext(ctx, "id")
|
||||
id, err := parseOrganizationID(idStr)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, fmt.Sprintf("invalid organization id: %s", err.Error()), s.Logger)
|
||||
return
|
||||
}
|
||||
id := httprouter.GetParamFromContext(ctx, "id")
|
||||
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
|
||||
if err != nil {
|
||||
|
@ -202,12 +191,7 @@ func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
ctx := r.Context()
|
||||
idStr := httprouter.GetParamFromContext(ctx, "id")
|
||||
id, err := parseOrganizationID(idStr)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, fmt.Sprintf("invalid organization id: %s", err.Error()), s.Logger)
|
||||
return
|
||||
}
|
||||
id := httprouter.GetParamFromContext(ctx, "id")
|
||||
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
|
||||
if err != nil {
|
||||
|
@ -242,12 +226,7 @@ func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
|
|||
// RemoveOrganization removes an organization in the organizations store
|
||||
func (s *Service) RemoveOrganization(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
idStr := httprouter.GetParamFromContext(ctx, "id")
|
||||
id, err := parseOrganizationID(idStr)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, fmt.Sprintf("invalid organization id: %s", err.Error()), s.Logger)
|
||||
return
|
||||
}
|
||||
id := httprouter.GetParamFromContext(ctx, "id")
|
||||
|
||||
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
|
||||
if err != nil {
|
||||
|
|
|
@ -50,9 +50,9 @@ func TestService_OrganizationID(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case 1337:
|
||||
case "1337":
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
Public: false,
|
||||
}, nil
|
||||
|
@ -139,12 +139,12 @@ func TestService_Organizations(t *testing.T) {
|
|||
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
|
||||
return []chronograf.Organization{
|
||||
chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
Public: false,
|
||||
},
|
||||
chronograf.Organization{
|
||||
ID: 100,
|
||||
ID: "100",
|
||||
Name: "The Bad Place",
|
||||
Public: false,
|
||||
},
|
||||
|
@ -228,7 +228,7 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: false,
|
||||
|
@ -262,7 +262,7 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 0,
|
||||
ID: "0",
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -294,7 +294,7 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.ViewerRoleName,
|
||||
Public: true,
|
||||
|
@ -328,7 +328,7 @@ func TestService_UpdateOrganization(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
DefaultRole: roles.MemberRoleName,
|
||||
Public: false,
|
||||
|
@ -475,9 +475,9 @@ func TestService_RemoveOrganization(t *testing.T) {
|
|||
},
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
switch *q.ID {
|
||||
case 1337:
|
||||
case "1337":
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
}, nil
|
||||
default:
|
||||
|
@ -573,7 +573,7 @@ func TestService_NewOrganization(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
Public: false,
|
||||
}, nil
|
||||
|
@ -612,7 +612,7 @@ func TestService_NewOrganization(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
}, nil
|
||||
},
|
||||
|
@ -654,7 +654,7 @@ func TestService_NewOrganization(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 1337,
|
||||
ID: "1337",
|
||||
Name: "The Good Place",
|
||||
}, nil
|
||||
},
|
||||
|
|
|
@ -62,7 +62,6 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
|
|||
invalidJSON(w, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := QueriesResponse{
|
||||
Queries: make([]QueryResponse, len(req.Queries)),
|
||||
}
|
||||
|
|
|
@ -35,7 +35,9 @@ type getRoutesResponse struct {
|
|||
Mappings string `json:"mappings"` // Location of the application mappings endpoint
|
||||
Sources string `json:"sources"` // Location of the sources endpoint
|
||||
Me string `json:"me"` // Location of the me endpoint
|
||||
Environment string `json:"environment"` // Location of the environement endpoint
|
||||
Dashboards string `json:"dashboards"` // Location of the dashboards endpoint
|
||||
Config getConfigLinksResponse `json:"config"` // Location of the config endpoint and its various sections
|
||||
Auth []AuthRoute `json:"auth"` // Location of all auth routes.
|
||||
Logout *string `json:"logout,omitempty"` // Location of the logout route for all auth routes
|
||||
ExternalLinks getExternalLinksResponse `json:"external"` // All external links for the client to use
|
||||
|
@ -66,9 +68,14 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
Users: "/chronograf/v1/users",
|
||||
Organizations: "/chronograf/v1/organizations",
|
||||
Me: "/chronograf/v1/me",
|
||||
Environment: "/chronograf/v1/env",
|
||||
Mappings: "/chronograf/v1/mappings",
|
||||
Dashboards: "/chronograf/v1/dashboards",
|
||||
Auth: make([]AuthRoute, len(a.AuthRoutes)), // We want to return at least an empty array, rather than null
|
||||
Config: getConfigLinksResponse{
|
||||
Self: "/chronograf/v1/config",
|
||||
Auth: "/chronograf/v1/config/auth",
|
||||
},
|
||||
Auth: make([]AuthRoute, len(a.AuthRoutes)), // We want to return at least an empty array, rather than null
|
||||
ExternalLinks: getExternalLinksResponse{
|
||||
StatusFeed: &a.StatusFeed,
|
||||
CustomLinks: customLinks,
|
||||
|
|
|
@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutes not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[],"external":{"statusFeed":""}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
@ -67,7 +67,7 @@ func TestAllRoutesWithAuth(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
@ -100,7 +100,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
|
129
server/server.go
129
server/server.go
|
@ -16,10 +16,10 @@ import (
|
|||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/bolt"
|
||||
idgen "github.com/influxdata/chronograf/id"
|
||||
"github.com/influxdata/chronograf/influx"
|
||||
clog "github.com/influxdata/chronograf/log"
|
||||
"github.com/influxdata/chronograf/oauth2"
|
||||
"github.com/influxdata/chronograf/uuid"
|
||||
client "github.com/influxdata/usage-client/v1"
|
||||
flags "github.com/jessevdk/go-flags"
|
||||
"github.com/tylerb/graceful"
|
||||
|
@ -52,12 +52,11 @@ type Server struct {
|
|||
|
||||
NewSources string `long:"new-sources" description:"Config for adding a new InfluxDB source and Kapacitor server, in JSON as an array of objects, and surrounded by single quotes. E.g. --new-sources='[{\"influxdb\":{\"name\":\"Influx 1\",\"username\":\"user1\",\"password\":\"pass1\",\"url\":\"http://localhost:8086\",\"metaUrl\":\"http://metaurl.com\",\"type\":\"influx-enterprise\",\"insecureSkipVerify\":false,\"default\":true,\"telegraf\":\"telegraf\",\"sharedSecret\":\"cubeapples\"},\"kapacitor\":{\"name\":\"Kapa 1\",\"url\":\"http://localhost:9092\",\"active\":true}}]'" env:"NEW_SOURCES" hidden:"true"`
|
||||
|
||||
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"`
|
||||
TokenSecret string `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"`
|
||||
AuthDuration time.Duration `long:"auth-duration" default:"720h" description:"Total duration of cookie life for authentication (in hours). 0 means authentication expires on browser close." env:"AUTH_DURATION"`
|
||||
SuperAdminFirstUserOnly bool `long:"superadmin-first-user-only" description:"All new users will not be given the SuperAdmin status" env:"SUPERADMIN_FIRST_USER_ONLY"`
|
||||
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
|
||||
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"`
|
||||
CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned dashboards and application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"`
|
||||
TokenSecret string `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"`
|
||||
AuthDuration time.Duration `long:"auth-duration" default:"720h" description:"Total duration of cookie life for authentication (in hours). 0 means authentication expires on browser close." env:"AUTH_DURATION"`
|
||||
|
||||
GithubClientID string `short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID"`
|
||||
GithubClientSecret string `short:"s" long:"github-client-secret" description:"Github Client Secret for OAuth 2 support" env:"GH_CLIENT_SECRET"`
|
||||
|
@ -87,15 +86,16 @@ type Server struct {
|
|||
Auth0ClientSecret string `long:"auth0-client-secret" description:"Auth0 Client Secret for OAuth2 support" env:"AUTH0_CLIENT_SECRET"`
|
||||
Auth0Organizations []string `long:"auth0-organizations" description:"Auth0 organizations permitted to access Chronograf (comma separated)" env:"AUTH0_ORGS" env-delim:","`
|
||||
|
||||
StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://www.influxdata.com/feed/json" env:"STATUS_FEED_URL"`
|
||||
CustomLinks map[string]string `long:"custom-link" description:"Custom link to be added to the client User menu. Multiple links can be added by using multiple of the same flag with different 'name:url' values, or as an environment variable with comma-separated 'name:url' values. E.g. via flags: '--custom-link=InfluxData:https://www.influxdata.com --custom-link=Chronograf:https://github.com/influxdata/chronograf'. E.g. via environment variable: 'export CUSTOM_LINKS=InfluxData:https://www.influxdata.com,Chronograf:https://github.com/influxdata/chronograf'" env:"CUSTOM_LINKS" env-delim:","`
|
||||
StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://www.influxdata.com/feed/json" env:"STATUS_FEED_URL"`
|
||||
CustomLinks map[string]string `long:"custom-link" description:"Custom link to be added to the client User menu. Multiple links can be added by using multiple of the same flag with different 'name:url' values, or as an environment variable with comma-separated 'name:url' values. E.g. via flags: '--custom-link=InfluxData:https://www.influxdata.com --custom-link=Chronograf:https://github.com/influxdata/chronograf'. E.g. via environment variable: 'export CUSTOM_LINKS=InfluxData:https://www.influxdata.com,Chronograf:https://github.com/influxdata/chronograf'" env:"CUSTOM_LINKS" env-delim:","`
|
||||
TelegrafSystemInterval time.Duration `long:"telegraf-system-interval" default:"1m" description:"Duration used in the GROUP BY time interval for the hosts list" env:"TELEGRAF_SYSTEM_INTERVAL"`
|
||||
|
||||
ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"`
|
||||
LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
|
||||
Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"`
|
||||
PrefixRoutes bool `long:"prefix-routes" description:"Force chronograf server to require that all requests to it are prefixed with the value set in --basepath" env:"PREFIX_ROUTES"`
|
||||
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
|
||||
BuildInfo BuildInfo
|
||||
BuildInfo chronograf.BuildInfo
|
||||
Listener net.Listener
|
||||
handler http.Handler
|
||||
}
|
||||
|
@ -123,6 +123,7 @@ func (s *Server) UseHeroku() bool {
|
|||
return s.TokenSecret != "" && s.HerokuClientID != "" && s.HerokuSecret != ""
|
||||
}
|
||||
|
||||
// UseAuth0 validates the CLI parameters to enable Auth0 oauth support
|
||||
func (s *Server) UseAuth0() bool {
|
||||
return s.Auth0ClientID != "" && s.Auth0ClientSecret != ""
|
||||
}
|
||||
|
@ -231,12 +232,6 @@ func (s *Server) genericRedirectURL() string {
|
|||
return publicURL.String()
|
||||
}
|
||||
|
||||
// BuildInfo is sent to the usage client to track versions and commits
|
||||
type BuildInfo struct {
|
||||
Version string
|
||||
Commit string
|
||||
}
|
||||
|
||||
func (s *Server) useAuth() bool {
|
||||
return s.UseGithub() || s.UseGoogle() || s.UseHeroku() || s.UseGenericOAuth2() || s.UseAuth0()
|
||||
}
|
||||
|
@ -276,24 +271,52 @@ func (s *Server) NewListener() (net.Listener, error) {
|
|||
return listener, nil
|
||||
}
|
||||
|
||||
type builders struct {
|
||||
Layouts LayoutBuilder
|
||||
Sources SourcesBuilder
|
||||
Kapacitors KapacitorBuilder
|
||||
Dashboards DashboardBuilder
|
||||
Organizations OrganizationBuilder
|
||||
}
|
||||
|
||||
func (s *Server) newBuilders(logger chronograf.Logger) builders {
|
||||
return builders{
|
||||
Layouts: &MultiLayoutBuilder{
|
||||
Logger: logger,
|
||||
UUID: &idgen.UUID{},
|
||||
CannedPath: s.CannedPath,
|
||||
},
|
||||
Dashboards: &MultiDashboardBuilder{
|
||||
Logger: logger,
|
||||
ID: idgen.NewTime(),
|
||||
Path: s.CannedPath,
|
||||
},
|
||||
Sources: &MultiSourceBuilder{
|
||||
InfluxDBURL: s.InfluxDBURL,
|
||||
InfluxDBUsername: s.InfluxDBUsername,
|
||||
InfluxDBPassword: s.InfluxDBPassword,
|
||||
Logger: logger,
|
||||
ID: idgen.NewTime(),
|
||||
Path: s.CannedPath,
|
||||
},
|
||||
Kapacitors: &MultiKapacitorBuilder{
|
||||
KapacitorURL: s.KapacitorURL,
|
||||
KapacitorUsername: s.KapacitorUsername,
|
||||
KapacitorPassword: s.KapacitorPassword,
|
||||
Logger: logger,
|
||||
ID: idgen.NewTime(),
|
||||
Path: s.CannedPath,
|
||||
},
|
||||
Organizations: &MultiOrganizationBuilder{
|
||||
Logger: logger,
|
||||
Path: s.CannedPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Serve starts and runs the chronograf server
|
||||
func (s *Server) Serve(ctx context.Context) error {
|
||||
logger := clog.New(clog.ParseLevel(s.LogLevel))
|
||||
layoutBuilder := &MultiLayoutBuilder{
|
||||
Logger: logger,
|
||||
UUID: &uuid.V4{},
|
||||
CannedPath: s.CannedPath,
|
||||
}
|
||||
sourcesBuilder := &MultiSourceBuilder{
|
||||
InfluxDBURL: s.InfluxDBURL,
|
||||
InfluxDBUsername: s.InfluxDBUsername,
|
||||
InfluxDBPassword: s.InfluxDBPassword,
|
||||
}
|
||||
kapacitorBuilder := &MultiKapacitorBuilder{
|
||||
KapacitorURL: s.KapacitorURL,
|
||||
KapacitorUsername: s.KapacitorUsername,
|
||||
KapacitorPassword: s.KapacitorPassword,
|
||||
}
|
||||
_, err := NewCustomLinks(s.CustomLinks)
|
||||
if err != nil {
|
||||
logger.
|
||||
|
@ -302,8 +325,10 @@ func (s *Server) Serve(ctx context.Context) error {
|
|||
Error(err)
|
||||
return err
|
||||
}
|
||||
service := openService(ctx, s.BoltPath, layoutBuilder, sourcesBuilder, kapacitorBuilder, logger, s.useAuth())
|
||||
service.SuperAdminFirstUserOnly = s.SuperAdminFirstUserOnly
|
||||
service := openService(ctx, s.BuildInfo, s.BoltPath, s.newBuilders(logger), logger, s.useAuth())
|
||||
service.Env = chronograf.Environment{
|
||||
TelegrafSystemInterval: s.TelegrafSystemInterval,
|
||||
}
|
||||
if err := service.HandleNewSources(ctx, s.NewSources); err != nil {
|
||||
logger.
|
||||
WithField("component", "server").
|
||||
|
@ -398,17 +423,18 @@ func (s *Server) Serve(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, sBuilder SourcesBuilder, kapBuilder KapacitorBuilder, logger chronograf.Logger, useAuth bool) Service {
|
||||
func openService(ctx context.Context, buildInfo chronograf.BuildInfo, boltPath string, builder builders, logger chronograf.Logger, useAuth bool) Service {
|
||||
db := bolt.NewClient()
|
||||
db.Path = boltPath
|
||||
if err := db.Open(ctx); err != nil {
|
||||
|
||||
if err := db.Open(ctx, logger, buildInfo, bolt.WithBackup()); err != nil {
|
||||
logger.
|
||||
WithField("component", "boltstore").
|
||||
Error("Unable to open boltdb; is there a chronograf already running? ", err)
|
||||
Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
layouts, err := lBuilder.Build(db.LayoutsStore)
|
||||
layouts, err := builder.Layouts.Build(db.LayoutsStore)
|
||||
if err != nil {
|
||||
logger.
|
||||
WithField("component", "LayoutsStore").
|
||||
|
@ -416,7 +442,14 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
sources, err := sBuilder.Build(db.SourcesStore)
|
||||
dashboards, err := builder.Dashboards.Build(db.DashboardsStore)
|
||||
if err != nil {
|
||||
logger.
|
||||
WithField("component", "DashboardsStore").
|
||||
Error("Unable to construct a MultiDashboardsStore", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
sources, err := builder.Sources.Build(db.SourcesStore)
|
||||
if err != nil {
|
||||
logger.
|
||||
WithField("component", "SourcesStore").
|
||||
|
@ -424,7 +457,7 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
kapacitors, err := kapBuilder.Build(db.ServersStore)
|
||||
kapacitors, err := builder.Kapacitors.Build(db.ServersStore)
|
||||
if err != nil {
|
||||
logger.
|
||||
WithField("component", "KapacitorStore").
|
||||
|
@ -432,16 +465,24 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
organizations, err := builder.Organizations.Build(db.OrganizationsStore)
|
||||
if err != nil {
|
||||
logger.
|
||||
WithField("component", "OrganizationsStore").
|
||||
Error("Unable to construct a MultiOrganizationStore", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return Service{
|
||||
TimeSeriesClient: &InfluxClient{},
|
||||
Store: &Store{
|
||||
LayoutsStore: layouts,
|
||||
DashboardsStore: dashboards,
|
||||
SourcesStore: sources,
|
||||
ServersStore: kapacitors,
|
||||
OrganizationsStore: organizations,
|
||||
UsersStore: db.UsersStore,
|
||||
OrganizationsStore: db.OrganizationsStore,
|
||||
LayoutsStore: layouts,
|
||||
DashboardsStore: db.DashboardsStore,
|
||||
//OrganizationUsersStore: organizations.NewUsersStore(db.UsersStore),
|
||||
ConfigStore: db.ConfigStore,
|
||||
},
|
||||
Logger: logger,
|
||||
UseAuth: useAuth,
|
||||
|
@ -450,7 +491,7 @@ func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, s
|
|||
}
|
||||
|
||||
// reportUsageStats starts periodic server reporting.
|
||||
func reportUsageStats(bi BuildInfo, logger chronograf.Logger) {
|
||||
func reportUsageStats(bi chronograf.BuildInfo, logger chronograf.Logger) {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
serverID := strconv.FormatUint(uint64(rand.Int63()), 10)
|
||||
reporter := client.New("")
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
)
|
||||
|
||||
func WithContext(ctx context.Context, r *http.Request, kv map[string]string) *http.Request {
|
||||
params := make(httprouter.Params, 0, len(kv))
|
||||
for k, v := range kv {
|
||||
params = append(params, httprouter.Param{
|
||||
Key: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
return r.WithContext(httprouter.WithParams(ctx, params))
|
||||
}
|
|
@ -11,12 +11,12 @@ import (
|
|||
|
||||
// Service handles REST calls to the persistence
|
||||
type Service struct {
|
||||
Store DataStore
|
||||
TimeSeriesClient TimeSeriesClient
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
SuperAdminFirstUserOnly bool
|
||||
Databases chronograf.Databases
|
||||
Store DataStore
|
||||
TimeSeriesClient TimeSeriesClient
|
||||
Logger chronograf.Logger
|
||||
UseAuth bool
|
||||
Env chronograf.Environment
|
||||
Databases chronograf.Databases
|
||||
}
|
||||
|
||||
// TimeSeriesClient returns the correct client for a time series database.
|
||||
|
|
|
@ -78,7 +78,7 @@ func (s *Service) NewSource(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := ValidSourceRequest(&src, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
|
||||
if err := ValidSourceRequest(&src, defaultOrg.ID); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ func (s *Service) UpdateSource(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := ValidSourceRequest(&src, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
|
||||
if err := ValidSourceRequest(&src, defaultOrg.ID); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -346,7 +346,7 @@ func (s *Service) HandleNewSources(ctx context.Context, input string) error {
|
|||
}
|
||||
|
||||
for _, sk := range srcsKaps {
|
||||
if err := ValidSourceRequest(&sk.Source, fmt.Sprintf("%d", defaultOrg.ID)); err != nil {
|
||||
if err := ValidSourceRequest(&sk.Source, defaultOrg.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add any new sources and kapacitors as specified via server flag
|
||||
|
|
|
@ -89,6 +89,7 @@ type DataStore interface {
|
|||
Users(ctx context.Context) chronograf.UsersStore
|
||||
Organizations(ctx context.Context) chronograf.OrganizationsStore
|
||||
Dashboards(ctx context.Context) chronograf.DashboardsStore
|
||||
Config(ctx context.Context) chronograf.ConfigStore
|
||||
}
|
||||
|
||||
// ensure that Store implements a DataStore
|
||||
|
@ -102,6 +103,7 @@ type Store struct {
|
|||
UsersStore chronograf.UsersStore
|
||||
DashboardsStore chronograf.DashboardsStore
|
||||
OrganizationsStore chronograf.OrganizationsStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
}
|
||||
|
||||
// Sources returns a noop.SourcesStore if the context has no organization specified
|
||||
|
@ -178,3 +180,14 @@ func (s *Store) Organizations(ctx context.Context) chronograf.OrganizationsStore
|
|||
}
|
||||
return &noop.OrganizationsStore{}
|
||||
}
|
||||
|
||||
// Config returns the underlying ConfigStore.
|
||||
func (s *Store) Config(ctx context.Context) chronograf.ConfigStore {
|
||||
if isServer := hasServerContext(ctx); isServer {
|
||||
return s.ConfigStore
|
||||
}
|
||||
if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin {
|
||||
return s.ConfigStore
|
||||
}
|
||||
return &noop.ConfigStore{}
|
||||
}
|
||||
|
|
|
@ -237,7 +237,7 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
OrganizationsStore chronograf.OrganizationsStore
|
||||
}
|
||||
type args struct {
|
||||
orgID uint64
|
||||
orgID string
|
||||
serverContext bool
|
||||
organization string
|
||||
user *chronograf.User
|
||||
|
@ -259,7 +259,7 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 21,
|
||||
ID: "21",
|
||||
Name: "my sweet name",
|
||||
DefaultRole: "viewer",
|
||||
}, nil
|
||||
|
@ -268,11 +268,11 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
},
|
||||
args: args{
|
||||
serverContext: true,
|
||||
orgID: 21,
|
||||
orgID: "21",
|
||||
},
|
||||
wants: wants{
|
||||
organization: &chronograf.Organization{
|
||||
ID: 21,
|
||||
ID: "21",
|
||||
Name: "my sweet name",
|
||||
DefaultRole: "viewer",
|
||||
},
|
||||
|
@ -284,7 +284,7 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 21,
|
||||
ID: "21",
|
||||
Name: "my sweet name",
|
||||
DefaultRole: "viewer",
|
||||
}, nil
|
||||
|
@ -299,11 +299,11 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
},
|
||||
orgID: 21,
|
||||
orgID: "21",
|
||||
},
|
||||
wants: wants{
|
||||
organization: &chronograf.Organization{
|
||||
ID: 21,
|
||||
ID: "21",
|
||||
Name: "my sweet name",
|
||||
DefaultRole: "viewer",
|
||||
},
|
||||
|
@ -315,7 +315,7 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 21,
|
||||
ID: "21",
|
||||
Name: "my sweet name",
|
||||
DefaultRole: "viewer",
|
||||
}, nil
|
||||
|
@ -329,7 +329,7 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
},
|
||||
orgID: 21,
|
||||
orgID: "21",
|
||||
},
|
||||
wants: wants{
|
||||
err: true,
|
||||
|
@ -341,7 +341,7 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 21,
|
||||
ID: "22",
|
||||
Name: "my sweet name",
|
||||
DefaultRole: "viewer",
|
||||
}, nil
|
||||
|
@ -355,12 +355,12 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
},
|
||||
organization: "21",
|
||||
orgID: 21,
|
||||
organization: "22",
|
||||
orgID: "22",
|
||||
},
|
||||
wants: wants{
|
||||
organization: &chronograf.Organization{
|
||||
ID: 21,
|
||||
ID: "22",
|
||||
Name: "my sweet name",
|
||||
DefaultRole: "viewer",
|
||||
},
|
||||
|
@ -372,7 +372,7 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
OrganizationsStore: &mocks.OrganizationsStore{
|
||||
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
|
||||
return &chronograf.Organization{
|
||||
ID: 22,
|
||||
ID: "22",
|
||||
Name: "my sweet name",
|
||||
DefaultRole: "viewer",
|
||||
}, nil
|
||||
|
@ -387,7 +387,7 @@ func TestStore_OrganizationsAdd(t *testing.T) {
|
|||
Scheme: "oauth2",
|
||||
},
|
||||
organization: "21",
|
||||
orgID: 21,
|
||||
orgID: "21",
|
||||
},
|
||||
wants: wants{
|
||||
err: true,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"info": {
|
||||
"title": "Chronograf",
|
||||
"description": "API endpoints for Chronograf",
|
||||
"version": "1.4.0.0-beta1"
|
||||
"version": "1.4.0.0"
|
||||
},
|
||||
"schemes": ["http"],
|
||||
"basePath": "/chronograf/v1",
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/uuid"
|
||||
idgen "github.com/influxdata/chronograf/id"
|
||||
)
|
||||
|
||||
// ValidTemplateRequest checks if the request sent to the server is the correct format.
|
||||
|
@ -111,7 +111,7 @@ func (s *Service) NewTemplate(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
ids := uuid.V4{}
|
||||
ids := idgen.UUID{}
|
||||
tid, err := ids.Generate()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Error creating template ID for dashboard %d: %v", id, err)
|
||||
|
|
|
@ -54,9 +54,6 @@ func (r *userRequest) ValidRoles() error {
|
|||
if r.Organization == "" {
|
||||
return fmt.Errorf("no organization was provided")
|
||||
}
|
||||
if _, err := parseOrganizationID(r.Organization); err != nil {
|
||||
return fmt.Errorf("failed to parse organization ID: %v", err)
|
||||
}
|
||||
if _, ok := orgs[r.Organization]; ok {
|
||||
return fmt.Errorf("duplicate organization %q in roles", r.Organization)
|
||||
}
|
||||
|
@ -157,6 +154,14 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
serverCtx := serverContext(ctx)
|
||||
cfg, err := s.Store.Config(serverCtx).Get(serverCtx)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user := &chronograf.User{
|
||||
Name: req.Name,
|
||||
Provider: req.Provider,
|
||||
|
@ -164,6 +169,10 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
|
|||
Roles: req.Roles,
|
||||
}
|
||||
|
||||
if cfg.Auth.SuperAdminNewUsers {
|
||||
req.SuperAdmin = true
|
||||
}
|
||||
|
||||
if err := setSuperAdmin(ctx, req, user); err != nil {
|
||||
Error(w, http.StatusUnauthorized, err.Error(), s.Logger)
|
||||
return
|
||||
|
@ -264,6 +273,21 @@ func (s *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Don't allow SuperAdmins to modify their own SuperAdmin status.
|
||||
// Allowing them to do so could result in an application where there
|
||||
// are no super admins.
|
||||
ctxUser, ok := hasUserContext(ctx)
|
||||
if !ok {
|
||||
Error(w, http.StatusInternalServerError, "failed to retrieve user from context", s.Logger)
|
||||
return
|
||||
}
|
||||
// If the user being updated is the user making the request and they are
|
||||
// changing their SuperAdmin status, return an unauthorized error
|
||||
if ctxUser.ID == u.ID && u.SuperAdmin == true && req.SuperAdmin == false {
|
||||
Error(w, http.StatusUnauthorized, "user cannot modify their own SuperAdmin status", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := setSuperAdmin(ctx, req, u); err != nil {
|
||||
Error(w, http.StatusUnauthorized, err.Error(), s.Logger)
|
||||
return
|
||||
|
@ -312,7 +336,7 @@ func setSuperAdmin(ctx context.Context, req userRequest, user *chronograf.User)
|
|||
} else if !isSuperAdmin && (user.SuperAdmin != req.SuperAdmin) {
|
||||
// If req.SuperAdmin has been set, and the request was not made with the SuperAdmin
|
||||
// context, return error
|
||||
return fmt.Errorf("User does not have authorization required to set SuperAdmin status")
|
||||
return fmt.Errorf("User does not have authorization required to set SuperAdmin status. See https://github.com/influxdata/chronograf/issues/2601 for more information.")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -112,8 +112,9 @@ func TestService_UserID(t *testing.T) {
|
|||
|
||||
func TestService_NewUser(t *testing.T) {
|
||||
type fields struct {
|
||||
UsersStore chronograf.UsersStore
|
||||
Logger chronograf.Logger
|
||||
UsersStore chronograf.UsersStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
Logger chronograf.Logger
|
||||
}
|
||||
type args struct {
|
||||
w *httptest.ResponseRecorder
|
||||
|
@ -146,6 +147,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -189,6 +197,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -241,6 +256,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -291,6 +313,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -305,7 +334,7 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":401,"message":"User does not have authorization required to set SuperAdmin status"}`,
|
||||
wantBody: `{"code":401,"message":"User does not have authorization required to set SuperAdmin status. See https://github.com/influxdata/chronograf/issues/2601 for more information."}`,
|
||||
},
|
||||
{
|
||||
name: "Create a new SuperAdmin User - as superadmin",
|
||||
|
@ -332,6 +361,13 @@ func TestService_NewUser(t *testing.T) {
|
|||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
return &chronograf.User{
|
||||
|
@ -349,13 +385,56 @@ func TestService_NewUser(t *testing.T) {
|
|||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1338","superAdmin":true,"name":"bob","provider":"github","scheme":"oauth2","roles":[],"links":{"self":"/chronograf/v1/users/1338"}}`,
|
||||
},
|
||||
{
|
||||
name: "Create a new User with SuperAdminNewUsers: true in ConfigStore",
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"POST",
|
||||
"http://any.url",
|
||||
nil,
|
||||
),
|
||||
user: &userRequest{
|
||||
Name: "bob",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
},
|
||||
userKeyUser: &chronograf.User{
|
||||
ID: 0,
|
||||
Name: "coolUser",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
},
|
||||
},
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
ConfigStore: &mocks.ConfigStore{
|
||||
Config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
UsersStore: &mocks.UsersStore{
|
||||
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
|
||||
user.ID = 1338
|
||||
return user, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStatus: http.StatusCreated,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"id":"1338","superAdmin":true,"name":"bob","provider":"github","scheme":"oauth2","roles":[],"links":{"self":"/chronograf/v1/users/1338"}}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
UsersStore: tt.fields.UsersStore,
|
||||
ConfigStore: tt.fields.ConfigStore,
|
||||
},
|
||||
Logger: tt.fields.Logger,
|
||||
}
|
||||
|
@ -588,6 +667,13 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
"http://any.url",
|
||||
nil,
|
||||
),
|
||||
userKeyUser: &chronograf.User{
|
||||
ID: 0,
|
||||
Name: "coolUser",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: false,
|
||||
},
|
||||
user: &userRequest{
|
||||
ID: 1336,
|
||||
Roles: []chronograf.Role{
|
||||
|
@ -636,6 +722,13 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
"http://any.url",
|
||||
nil,
|
||||
),
|
||||
userKeyUser: &chronograf.User{
|
||||
ID: 0,
|
||||
Name: "coolUser",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: false,
|
||||
},
|
||||
user: &userRequest{
|
||||
ID: 1336,
|
||||
Roles: []chronograf.Role{
|
||||
|
@ -707,6 +800,119 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
wantContentType: "application/json",
|
||||
wantBody: `{"code":422,"message":"duplicate organization \"1\" in roles"}`,
|
||||
},
|
||||
{
|
||||
name: "SuperAdmin modifying their own SuperAdmin Status - user missing from context",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
switch *q.ID {
|
||||
case 1336:
|
||||
return &chronograf.User{
|
||||
ID: 1336,
|
||||
Name: "bobbetta",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.EditorRoleName,
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("User with ID %d not found", *q.ID)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"PATCH",
|
||||
"http://any.url",
|
||||
nil,
|
||||
),
|
||||
user: &userRequest{
|
||||
ID: 1336,
|
||||
SuperAdmin: false,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.AdminRoleName,
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
id: "1336",
|
||||
wantStatus: http.StatusInternalServerError,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":500,"message":"failed to retrieve user from context"}`,
|
||||
},
|
||||
{
|
||||
name: "SuperAdmin modifying their own SuperAdmin Status",
|
||||
fields: fields{
|
||||
Logger: log.New(log.DebugLevel),
|
||||
UsersStore: &mocks.UsersStore{
|
||||
UpdateF: func(ctx context.Context, user *chronograf.User) error {
|
||||
return nil
|
||||
},
|
||||
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
||||
switch *q.ID {
|
||||
case 1336:
|
||||
return &chronograf.User{
|
||||
ID: 1336,
|
||||
Name: "bobbetta",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.EditorRoleName,
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("User with ID %d not found", *q.ID)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
w: httptest.NewRecorder(),
|
||||
r: httptest.NewRequest(
|
||||
"PATCH",
|
||||
"http://any.url",
|
||||
nil,
|
||||
),
|
||||
user: &userRequest{
|
||||
ID: 1336,
|
||||
SuperAdmin: false,
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.AdminRoleName,
|
||||
Organization: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
userKeyUser: &chronograf.User{
|
||||
ID: 1336,
|
||||
Name: "coolUser",
|
||||
Provider: "github",
|
||||
Scheme: "oauth2",
|
||||
SuperAdmin: true,
|
||||
},
|
||||
},
|
||||
id: "1336",
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":401,"message":"user cannot modify their own SuperAdmin status"}`,
|
||||
},
|
||||
{
|
||||
name: "Update a SuperAdmin's Roles - without super admin context",
|
||||
fields: fields{
|
||||
|
@ -821,7 +1027,7 @@ func TestService_UpdateUser(t *testing.T) {
|
|||
id: "1336",
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantContentType: "application/json",
|
||||
wantBody: `{"code":401,"message":"User does not have authorization required to set SuperAdmin status"}`,
|
||||
wantBody: `{"code":401,"message":"User does not have authorization required to set SuperAdmin status. See https://github.com/influxdata/chronograf/issues/2601 for more information."}`,
|
||||
},
|
||||
{
|
||||
name: "Update a Chronograf user to super admin - with super admin context",
|
||||
|
@ -1077,25 +1283,6 @@ func TestUserRequest_ValidCreate(t *testing.T) {
|
|||
wantErr: false,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid - bad organization",
|
||||
args: args{
|
||||
u: &userRequest{
|
||||
ID: 1337,
|
||||
Name: "billietta",
|
||||
Provider: "auth0",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.EditorRoleName,
|
||||
Organization: "l", // this is the character L not integer One
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
err: fmt.Errorf("failed to parse organization ID: strconv.ParseUint: parsing \"l\": invalid syntax"),
|
||||
},
|
||||
{
|
||||
name: "Invalid – Name missing",
|
||||
args: args{
|
||||
|
@ -1240,25 +1427,6 @@ func TestUserRequest_ValidUpdate(t *testing.T) {
|
|||
wantErr: true,
|
||||
err: fmt.Errorf("No Roles to update"),
|
||||
},
|
||||
{
|
||||
name: "Invalid - bad organization",
|
||||
args: args{
|
||||
u: &userRequest{
|
||||
ID: 1337,
|
||||
Name: "billietta",
|
||||
Provider: "auth0",
|
||||
Scheme: "oauth2",
|
||||
Roles: []chronograf.Role{
|
||||
{
|
||||
Name: roles.EditorRoleName,
|
||||
Organization: "l", // this is the character L not integer One
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
err: fmt.Errorf("failed to parse organization ID: strconv.ParseUint: parsing \"l\": invalid syntax"),
|
||||
},
|
||||
{
|
||||
name: "Invalid - bad role name",
|
||||
args: args{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.4.0-0beta1",
|
||||
"version": "1.4.0-0",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
@ -103,7 +103,7 @@
|
|||
"bootstrap": "^3.3.7",
|
||||
"calculate-size": "^1.1.1",
|
||||
"classnames": "^2.2.3",
|
||||
"dygraphs": "influxdata/dygraphs",
|
||||
"dygraphs": "2.1.0",
|
||||
"eslint-plugin-babel": "^4.1.2",
|
||||
"fast.js": "^0.1.1",
|
||||
"fixed-data-table": "^0.6.1",
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
editUser,
|
||||
editRole,
|
||||
editDatabase,
|
||||
editRetentionPolicy,
|
||||
editRetentionPolicyRequested,
|
||||
loadRoles,
|
||||
loadPermissions,
|
||||
deleteRole,
|
||||
|
@ -203,7 +203,10 @@ describe('Admin.InfluxDB.Reducers', () => {
|
|||
|
||||
it('can edit a retention policy', () => {
|
||||
const updates = {name: 'rpOne', duration: '100y', replication: '42'}
|
||||
const actual = reducer(state, editRetentionPolicy(db1, rp1, updates))
|
||||
const actual = reducer(
|
||||
state,
|
||||
editRetentionPolicyRequested(db1, rp1, updates)
|
||||
)
|
||||
const expected = [{...db1, retentionPolicies: [{...rp1, ...updates}]}]
|
||||
|
||||
expect(actual.databases).to.deep.equal(expected)
|
||||
|
|
|
@ -258,5 +258,12 @@ describe('Presenters', () => {
|
|||
|
||||
expect(actual).to.equal('m1.derivative_mean_usage_system')
|
||||
})
|
||||
|
||||
it('returns a label of empty string if the query config is empty', () => {
|
||||
const query = defaultQueryConfig({id: 1})
|
||||
const actual = buildDefaultYLabel(query)
|
||||
|
||||
expect(actual).to.equal('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -208,8 +208,27 @@ export const removeDatabaseDeleteCode = database => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const editRetentionPolicy = (database, retentionPolicy, updates) => ({
|
||||
type: 'INFLUXDB_EDIT_RETENTION_POLICY',
|
||||
export const editRetentionPolicyRequested = (
|
||||
database,
|
||||
retentionPolicy,
|
||||
updates
|
||||
) => ({
|
||||
type: 'INFLUXDB_EDIT_RETENTION_POLICY_REQUESTED',
|
||||
payload: {
|
||||
database,
|
||||
retentionPolicy,
|
||||
updates,
|
||||
},
|
||||
})
|
||||
|
||||
export const editRetentionPolicyCompleted = syncRetentionPolicy
|
||||
|
||||
export const editRetentionPolicyFailed = (
|
||||
database,
|
||||
retentionPolicy,
|
||||
updates
|
||||
) => ({
|
||||
type: 'INFLUXDB_EDIT_RETENTION_POLICY_FAILED',
|
||||
payload: {
|
||||
database,
|
||||
retentionPolicy,
|
||||
|
@ -334,23 +353,21 @@ export const createRetentionPolicyAsync = (
|
|||
|
||||
export const updateRetentionPolicyAsync = (
|
||||
database,
|
||||
retentionPolicy,
|
||||
updates
|
||||
oldRP,
|
||||
newRP
|
||||
) => async dispatch => {
|
||||
try {
|
||||
dispatch(editRetentionPolicy(database, retentionPolicy, updates))
|
||||
const {data} = await updateRetentionPolicyAJAX(
|
||||
retentionPolicy.links.self,
|
||||
updates
|
||||
)
|
||||
dispatch(editRetentionPolicyRequested(database, oldRP, newRP))
|
||||
const {data} = await updateRetentionPolicyAJAX(oldRP.links.self, newRP)
|
||||
dispatch(editRetentionPolicyCompleted(database, oldRP, data))
|
||||
dispatch(
|
||||
publishAutoDismissingNotification(
|
||||
'success',
|
||||
'Retention policy updated successfully'
|
||||
)
|
||||
)
|
||||
dispatch(syncRetentionPolicy(database, retentionPolicy, data))
|
||||
} catch (error) {
|
||||
dispatch(editRetentionPolicyFailed(database, oldRP))
|
||||
dispatch(
|
||||
errorThrown(
|
||||
error,
|
||||
|
|
|
@ -7,44 +7,28 @@ import {
|
|||
} from 'src/auth/Authorized'
|
||||
|
||||
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
|
||||
import OrganizationsPage from 'src/admin/containers/OrganizationsPage'
|
||||
import UsersTable from 'src/admin/components/chronograf/UsersTable'
|
||||
import OrganizationsPage from 'src/admin/containers/chronograf/OrganizationsPage'
|
||||
import UsersPage from 'src/admin/containers/chronograf/UsersPage'
|
||||
|
||||
const ORGANIZATIONS_TAB_NAME = 'Organizations'
|
||||
const USERS_TAB_NAME = 'Users'
|
||||
|
||||
const AdminTabs = ({
|
||||
meRole,
|
||||
// UsersTable
|
||||
users,
|
||||
organization,
|
||||
onCreateUser,
|
||||
onUpdateUserRole,
|
||||
onUpdateUserSuperAdmin,
|
||||
onDeleteUser,
|
||||
meID,
|
||||
notify,
|
||||
me: {currentOrganization: meCurrentOrganization, role: meRole, id: meID},
|
||||
}) => {
|
||||
const tabs = [
|
||||
{
|
||||
requiredRole: SUPERADMIN_ROLE,
|
||||
type: ORGANIZATIONS_TAB_NAME,
|
||||
component: <OrganizationsPage currentOrganization={organization} />,
|
||||
component: (
|
||||
<OrganizationsPage meCurrentOrganization={meCurrentOrganization} />
|
||||
),
|
||||
},
|
||||
{
|
||||
requiredRole: ADMIN_ROLE,
|
||||
type: USERS_TAB_NAME,
|
||||
component: (
|
||||
<UsersTable
|
||||
users={users}
|
||||
organization={organization}
|
||||
onCreateUser={onCreateUser}
|
||||
onUpdateUserRole={onUpdateUserRole}
|
||||
onUpdateUserSuperAdmin={onUpdateUserSuperAdmin}
|
||||
onDeleteUser={onDeleteUser}
|
||||
meID={meID}
|
||||
notify={notify}
|
||||
/>
|
||||
<UsersPage meID={meID} meCurrentOrganization={meCurrentOrganization} />
|
||||
),
|
||||
},
|
||||
].filter(t => isUserAuthorized(meRole, t.requiredRole))
|
||||
|
@ -69,39 +53,17 @@ const AdminTabs = ({
|
|||
)
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
AdminTabs.propTypes = {
|
||||
meRole: string.isRequired,
|
||||
meID: string.isRequired,
|
||||
// UsersTable
|
||||
users: arrayOf(
|
||||
shape({
|
||||
id: string,
|
||||
links: shape({
|
||||
self: string.isRequired,
|
||||
}),
|
||||
name: string.isRequired,
|
||||
provider: string.isRequired,
|
||||
roles: arrayOf(
|
||||
shape({
|
||||
name: string.isRequired,
|
||||
organization: string.isRequired,
|
||||
})
|
||||
),
|
||||
scheme: string.isRequired,
|
||||
superAdmin: bool,
|
||||
})
|
||||
).isRequired,
|
||||
organization: shape({
|
||||
name: string.isRequired,
|
||||
me: shape({
|
||||
id: string.isRequired,
|
||||
role: string.isRequired,
|
||||
currentOrganization: shape({
|
||||
name: string.isRequired,
|
||||
id: string.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
onCreateUser: func.isRequired,
|
||||
onUpdateUserRole: func.isRequired,
|
||||
onUpdateUserSuperAdmin: func.isRequired,
|
||||
onDeleteUser: func.isRequired,
|
||||
notify: func.isRequired,
|
||||
}
|
||||
|
||||
export default AdminTabs
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react'
|
||||
|
||||
import UsersTableHeader from 'src/admin/components/chronograf/UsersTableHeader'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
|
||||
|
||||
const EmptyUsersTable = () => {
|
||||
const {
|
||||
colRole,
|
||||
colSuperAdmin,
|
||||
colProvider,
|
||||
colScheme,
|
||||
colActions,
|
||||
} = USERS_TABLE
|
||||
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<UsersTableHeader />
|
||||
<div className="panel-body">
|
||||
<table className="table table-highlight v-center chronograf-admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th style={{width: colRole}} className="align-with-col-text">
|
||||
Role
|
||||
</th>
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<th style={{width: colSuperAdmin}} className="text-center">
|
||||
SuperAdmin
|
||||
</th>
|
||||
</Authorized>
|
||||
<th style={{width: colProvider}}>Provider</th>
|
||||
<th style={{width: colScheme}}>Scheme</th>
|
||||
<th className="text-right" style={{width: colActions}} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody />
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmptyUsersTable
|
|
@ -2,9 +2,12 @@ import React, {Component, PropTypes} from 'react'
|
|||
|
||||
import uuid from 'node-uuid'
|
||||
|
||||
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
|
||||
import OrganizationsTableRowNew from 'src/admin/components/chronograf/OrganizationsTableRowNew'
|
||||
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
|
||||
import SlideToggle from 'shared/components/SlideToggle'
|
||||
|
||||
import {PUBLIC_TOOLTIP} from 'src/admin/constants/index'
|
||||
|
||||
|
@ -39,6 +42,8 @@ class OrganizationsTable extends Component {
|
|||
onChooseDefaultRole,
|
||||
onTogglePublic,
|
||||
currentOrganization,
|
||||
authConfig: {superAdminNewUsers},
|
||||
onChangeAuthConfig,
|
||||
} = this.props
|
||||
const {isCreatingOrganization} = this.state
|
||||
|
||||
|
@ -89,13 +94,35 @@ class OrganizationsTable extends Component {
|
|||
currentOrganization={currentOrganization}
|
||||
/>
|
||||
)}
|
||||
<Authorized requiredRole={SUPERADMIN_ROLE}>
|
||||
<table className="table v-center superadmin-config">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{width: 70}}>Config</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{width: 70}}>
|
||||
<SlideToggle
|
||||
size="xs"
|
||||
active={superAdminNewUsers}
|
||||
onToggle={onChangeAuthConfig('superAdminNewUsers')}
|
||||
/>
|
||||
</td>
|
||||
<td>All new users are SuperAdmins</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Authorized>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
OrganizationsTable.propTypes = {
|
||||
organizations: arrayOf(
|
||||
|
@ -113,5 +140,9 @@ OrganizationsTable.propTypes = {
|
|||
onRenameOrg: func.isRequired,
|
||||
onTogglePublic: func.isRequired,
|
||||
onChooseDefaultRole: func.isRequired,
|
||||
onChangeAuthConfig: func.isRequired,
|
||||
authConfig: shape({
|
||||
superAdminNewUsers: bool,
|
||||
}),
|
||||
}
|
||||
export default OrganizationsTable
|
||||
|
|
|
@ -28,7 +28,7 @@ class OrganizationsTableRowNew extends Component {
|
|||
}
|
||||
|
||||
handleInputChange = e => {
|
||||
this.setState({name: e.target.value.trim()})
|
||||
this.setState({name: e.target.value})
|
||||
}
|
||||
|
||||
handleInputFocus = e => {
|
||||
|
@ -39,7 +39,7 @@ class OrganizationsTableRowNew extends Component {
|
|||
const {onCreateOrganization} = this.props
|
||||
const {name, defaultRole} = this.state
|
||||
|
||||
onCreateOrganization({name, defaultRole})
|
||||
onCreateOrganization({name: name.trim(), defaultRole})
|
||||
}
|
||||
|
||||
handleChooseDefaultRole = role => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue