Merge branch 'master' into multiple-event-handlers

pull/2409/head
deniz kusefoglu 2018-01-04 18:14:06 -08:00
commit abdfd14bbd
162 changed files with 8178 additions and 1709 deletions

View File

@ -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}

29
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)!

View File

@ -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
}

83
bolt/build.go Normal file
View File

@ -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
}

54
bolt/build_test.go Normal file
View File

@ -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) {
// }

View File

@ -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))
}

71
bolt/config.go Normal file
View File

@ -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
})
}

105
bolt/config_test.go Normal file
View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
//

View 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

View File

@ -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

View File

@ -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
}

View File

@ -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{

View File

@ -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

View File

@ -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{

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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,
},

View File

@ -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

View File

@ -1,4 +1,4 @@
package canned
package filestore
import (
"context"

View File

@ -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,

210
filestore/dashboards.go Normal file
View File

@ -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
}

24
filestore/environ.go Normal file
View File

@ -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
}

29
filestore/environ_test.go Normal file
View File

@ -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)
}
})
}
}

184
filestore/kapacitors.go Normal file
View File

@ -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
}

122
filestore/organizations.go Normal file
View File

@ -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
}

184
filestore/sources.go Normal file
View File

@ -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
}

28
filestore/templates.go Normal file
View File

@ -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...)
}

View File

@ -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)
}
})
}
}

25
id/time.go Normal file
View File

@ -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
}

16
id/uuid.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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

8
integrations/testdata/example.kap vendored Normal file
View File

@ -0,0 +1,8 @@
{
"id": 5000,
"srcID": 5000,
"name": "Kapa 1",
"url": "http://localhost:9092",
"active": true,
"organization": "howdy"
}

5
integrations/testdata/example.org vendored Normal file
View File

@ -0,0 +1,5 @@
{
"id": "howdy",
"name": "An Organization",
"defaultRole": "viewer"
}

14
integrations/testdata/example.src vendored Normal file
View File

@ -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"
}

185
integrations/testdata/mydash.dashboard vendored Normal file
View File

@ -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"
}

View File

@ -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,
}

View File

@ -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
}

View File

@ -9,7 +9,6 @@ import (
func TestInterfaceImplementation(t *testing.T) {
var _ chronograf.ServersStore = &KapacitorStore{}
var _ chronograf.ServersStore = &MultiKapacitorStore{}
}
func TestKapacitorStoreAll(t *testing.T) {

View File

@ -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 {

28
mocks/config.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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
}

97
multistore/dashboards.go Normal file
View File

@ -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
}

97
multistore/kapacitors.go Normal file
View File

@ -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
}

View File

@ -0,0 +1,11 @@
package multistore
import (
"testing"
"github.com/influxdata/chronograf"
)
func TestInterfaceImplementation(t *testing.T) {
var _ chronograf.ServersStore = &KapacitorStore{}
}

View File

@ -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)

129
multistore/organizations.go Normal file
View File

@ -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, " "))
}

96
multistore/sources.go Normal file
View File

@ -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
}

26
noop/config.go Normal file
View File

@ -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")
}

View File

@ -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
}

View File

@ -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",

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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
},

View File

@ -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
}

View File

@ -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)
}

View File

@ -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))
}
})
}
}

123
server/config.go Normal file
View File

@ -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)
}

290
server/config_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -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
}

27
server/env.go Normal file
View File

@ -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)
}

70
server/env_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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 {

View File

@ -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
},

View File

@ -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)),
}

View File

@ -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,

View File

@ -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))

View File

@ -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("")

19
server/server_test.go Normal file
View File

@ -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))
}

View File

@ -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.

View File

@ -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

View File

@ -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{}
}

View File

@ -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,

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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{

View File

@ -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",

View File

@ -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)

View File

@ -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('')
})
})
})

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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