Add OrganizationConfigStore & refactor org config to be per org

As previously implemented, OrganizationConfig was a global
object. This refactor adds the organization id to context for
every request, even when auth is disabled, so that org id
can be used to get/update an organization config.

Along those lines, this also removes OrganizationConfigStore
.Initialize and replaces .Get with .FindOrCreate, handling
the creation of organization configs upon first attempted
access.

Co-authored-by: Jared Scheib <jared.scheib@gmail.com>
pull/10616/head
Jared Scheib 2018-07-05 18:40:52 -07:00
parent 9909bed41f
commit 429fe214e6
17 changed files with 2334 additions and 627 deletions

View File

@ -42,6 +42,7 @@ type Client struct {
OrganizationsStore *OrganizationsStore OrganizationsStore *OrganizationsStore
ConfigStore *ConfigStore ConfigStore *ConfigStore
MappingsStore *MappingsStore MappingsStore *MappingsStore
OrganizationConfigStore *OrganizationConfigStore
} }
// NewClient initializes all stores // NewClient initializes all stores
@ -62,6 +63,7 @@ func NewClient() *Client {
c.OrganizationsStore = &OrganizationsStore{client: c} c.OrganizationsStore = &OrganizationsStore{client: c}
c.ConfigStore = &ConfigStore{client: c} c.ConfigStore = &ConfigStore{client: c}
c.MappingsStore = &MappingsStore{client: c} c.MappingsStore = &MappingsStore{client: c}
c.OrganizationConfigStore = &OrganizationConfigStore{client: c}
return c return c
} }
@ -161,6 +163,10 @@ func (c *Client) initialize(ctx context.Context) error {
if _, err := tx.CreateBucketIfNotExists(MappingsBucket); err != nil { if _, err := tx.CreateBucketIfNotExists(MappingsBucket); err != nil {
return err return err
} }
// Always create OrganizationConfig bucket.
if _, err := tx.CreateBucketIfNotExists(OrganizationConfigBucket); err != nil {
return err
}
return nil return nil
}); err != nil { }); err != nil {
return err return err
@ -197,6 +203,9 @@ func (c *Client) migrate(ctx context.Context, build chronograf.BuildInfo) error
if err := c.MappingsStore.Migrate(ctx); err != nil { if err := c.MappingsStore.Migrate(ctx); err != nil {
return err return err
} }
if err := c.OrganizationConfigStore.Migrate(ctx); err != nil {
return err
}
MigrateAll(c) MigrateAll(c)
} }

View File

@ -749,9 +749,9 @@ func UnmarshalConfigPB(data []byte, c *Config) error {
// MarshalOrganizationConfig encodes a config to binary protobuf format. // MarshalOrganizationConfig encodes a config to binary protobuf format.
func MarshalOrganizationConfig(c *chronograf.OrganizationConfig) ([]byte, error) { func MarshalOrganizationConfig(c *chronograf.OrganizationConfig) ([]byte, error) {
var lv chronograf.LogViewerConfig columns := make([]*LogViewerColumn, len(c.LogViewer.Columns))
columns := make([]*LogViewerColumn, len(lv.Columns))
for i, column := range lv.Columns { for i, column := range c.LogViewer.Columns {
encodings := make([]*ColumnEncoding, len(column.Encodings)) encodings := make([]*ColumnEncoding, len(column.Encodings))
for j, e := range column.Encodings { for j, e := range column.Encodings {
@ -768,7 +768,9 @@ func MarshalOrganizationConfig(c *chronograf.OrganizationConfig) ([]byte, error)
Encodings: encodings, Encodings: encodings,
} }
} }
return MarshalOrganizationConfigPB(&OrganizationConfig{ return MarshalOrganizationConfigPB(&OrganizationConfig{
OrganizationID: c.OrganizationID,
LogViewer: &LogViewerConfig{ LogViewer: &LogViewerConfig{
Columns: columns, Columns: columns,
}, },
@ -783,13 +785,22 @@ func MarshalOrganizationConfigPB(c *OrganizationConfig) ([]byte, error) {
// UnmarshalOrganizationConfig decodes a config from binary protobuf data. // UnmarshalOrganizationConfig decodes a config from binary protobuf data.
func UnmarshalOrganizationConfig(data []byte, c *chronograf.OrganizationConfig) error { func UnmarshalOrganizationConfig(data []byte, c *chronograf.OrganizationConfig) error {
var pb OrganizationConfig var pb OrganizationConfig
if err := UnmarshalOrganizationConfigPB(data, &pb); err != nil { if err := UnmarshalOrganizationConfigPB(data, &pb); err != nil {
return err return err
} }
if pb.OrganizationID == "" {
return fmt.Errorf("Organization ID on organization config is nil")
}
if pb.LogViewer == nil { if pb.LogViewer == nil {
return fmt.Errorf("Log Viewer config is nil") return fmt.Errorf("Log Viewer config is nil")
} }
if pb.LogViewer.Columns == nil {
return fmt.Errorf("Log Viewer config Columns is nil")
}
c.OrganizationID = pb.OrganizationID
columns := make([]chronograf.LogViewerColumn, len(pb.LogViewer.Columns)) columns := make([]chronograf.LogViewerColumn, len(pb.LogViewer.Columns))

File diff suppressed because it is too large Load Diff

View File

@ -215,7 +215,8 @@ message AuthConfig {
} }
message OrganizationConfig { message OrganizationConfig {
LogViewerConfig LogViewer = 1; // LogViewer is the organization configuration for log viewer string OrganizationID = 1; // OrganizationID is the ID of the organization this config belogs to
LogViewerConfig LogViewer = 2; // LogViewer is the organization configuration for log viewer
} }
message LogViewerConfig { message LogViewerConfig {

View File

@ -12,30 +12,55 @@ import (
// Ensure OrganizationConfigStore implements chronograf.OrganizationConfigStore. // Ensure OrganizationConfigStore implements chronograf.OrganizationConfigStore.
var _ chronograf.OrganizationConfigStore = &OrganizationConfigStore{} var _ chronograf.OrganizationConfigStore = &OrganizationConfigStore{}
// ConfigBucket is used to store chronograf application state // OrganizationConfigBucket is used to store chronograf organization configurations
var ConfigBucket = []byte("ConfigV1") var OrganizationConfigBucket = []byte("OrganizationConfigV1")
// configID is the boltDB key where the configuration object is stored // OrganizationConfigStore uses bolt to store and retrieve organization configurations
var configID = []byte("config/v1")
// OrganizationConfigStore uses bolt to store and retrieve global
// application configuration
type OrganizationConfigStore struct { type OrganizationConfigStore struct {
client *Client client *Client
} }
func (s *OrganizationConfigStore) Migrate(ctx context.Context) error { func (s *OrganizationConfigStore) Migrate(ctx context.Context) error {
if _, err := s.Get(ctx); err != nil {
return s.Initialize(ctx)
}
return nil return nil
} }
func (s *OrganizationConfigStore) Initialize(ctx context.Context) error { // FindOrCreate gets an OrganizationConfig from the store or creates one if none exists for this organization
cfg := chronograf.OrganizationConfig{ func (s *OrganizationConfigStore) FindOrCreate(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
OrganizationConfig: chronograf.OrganizationConfig{ var cfg chronograf.OrganizationConfig
err := s.client.db.View(func(tx *bolt.Tx) error {
v := tx.Bucket(OrganizationConfigBucket).Get([]byte(orgID))
if v == nil {
cfg = newOrganizationConfig(orgID)
return nil
}
return internal.UnmarshalOrganizationConfig(v, &cfg)
})
LogViewer: chronograf.LogViewer{ if err != nil {
return nil, err
}
return &cfg, nil
}
// Update replaces the OrganizationConfig in the store
func (s *OrganizationConfigStore) Update(ctx context.Context, cfg *chronograf.OrganizationConfig) 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.MarshalOrganizationConfig(cfg); err != nil {
return err
} else if err := tx.Bucket(OrganizationConfigBucket).Put([]byte(cfg.OrganizationID), v); err != nil {
return err
}
return nil
})
}
func newOrganizationConfig(orgID string) chronograf.OrganizationConfig {
return chronograf.OrganizationConfig{
OrganizationID: orgID,
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
Name: "time", Name: "time",
@ -140,37 +165,5 @@ func (s *OrganizationConfigStore) Initialize(ctx context.Context) error {
}, },
}, },
}, },
},
} }
return s.Update(ctx, &cfg)
}
func (s *OrganizationConfigStore) Get(ctx context.Context) (*chronograf.OrganizationConfig, error) {
var cfg chronograf.OrganizationConfig
err := s.client.db.View(func(tx *bolt.Tx) error {
v := tx.Bucket(ConfigBucket).Get(configID)
if v == nil {
return chronograf.ErrConfigNotFound
}
return internal.UnmarshalOrganizationConfig(v, &cfg)
})
if err != nil {
return nil, err
}
return &cfg, nil
}
func (s *OrganizationConfigStore) Update(ctx context.Context, cfg *chronograf.OrganizationConfig) 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.MarshalOrganizationConfig(cfg); err != nil {
return err
} else if err := tx.Bucket(ConfigBucket).Put(configID, v); err != nil {
return err
}
return nil
})
} }

View File

@ -8,19 +8,29 @@ import (
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
) )
func TestConfig_Get(t *testing.T) { func TestOrganizationConfig_FindOrCreate(t *testing.T) {
type args struct {
organizationID string
}
type wants struct { type wants struct {
config *chronograf.OrganizationConfig organizationConfig *chronograf.OrganizationConfig
err error err error
} }
tests := []struct { tests := []struct {
name string name string
args args
addFirst bool
wants wants wants wants
}{ }{
{ {
name: "Get config", name: "Get non-existent default config from default org",
args: args{
organizationID: "default",
},
addFirst: false,
wants: wants{ wants: wants{
config: &chronograf.OrganizationConfig{ organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "default",
LogViewer: chronograf.LogViewerConfig{ LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
@ -129,6 +139,357 @@ func TestConfig_Get(t *testing.T) {
}, },
}, },
}, },
{
name: "Get non-existent default config from non-default org",
args: args{
organizationID: "1",
},
addFirst: false,
wants: wants{
organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "1",
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "time",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "hidden",
},
},
},
{
Name: "severity",
Position: 1,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "label",
Value: "icon",
},
{
Type: "label",
Value: "text",
},
},
},
{
Name: "timestamp",
Position: 2,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "message",
Position: 3,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "facility",
Position: 4,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "procid",
Position: 5,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Proc ID",
},
},
},
{
Name: "appname",
Position: 6,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Application",
},
},
},
{
Name: "host",
Position: 7,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
},
},
},
},
},
{
name: "Get existing/modified config from default org",
args: args{
organizationID: "default",
},
addFirst: true,
wants: wants{
organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "default",
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "time",
Position: 1,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "hidden",
},
},
},
{
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "hidden",
},
{
Type: "label",
Value: "icon",
},
{
Type: "label",
Value: "text",
},
},
},
{
Name: "timestamp",
Position: 2,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "message",
Position: 3,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "facility",
Position: 4,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "procid",
Position: 5,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Proc ID",
},
},
},
{
Name: "appname",
Position: 6,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Application",
},
},
},
{
Name: "host",
Position: 7,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
},
},
},
},
},
{
name: "Get existing/modified config from non-default org",
args: args{
organizationID: "1",
},
addFirst: true,
wants: wants{
organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "1",
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "time",
Position: 1,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "hidden",
},
},
},
{
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "hidden",
},
{
Type: "label",
Value: "icon",
},
{
Type: "label",
Value: "text",
},
},
},
{
Name: "timestamp",
Position: 2,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "message",
Position: 3,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "facility",
Position: 4,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "procid",
Position: 5,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Proc ID",
},
},
},
{
Name: "appname",
Position: 6,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Application",
},
},
},
{
Name: "host",
Position: 7,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
},
},
},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
client, err := NewTestClient() client, err := NewTestClient()
@ -138,23 +499,32 @@ func TestConfig_Get(t *testing.T) {
defer client.Close() defer client.Close()
s := client.OrganizationConfigStore s := client.OrganizationConfigStore
got, err := s.Get(context.Background())
if tt.addFirst {
if err := s.Update(context.Background(), tt.wants.organizationConfig); err != nil {
t.Fatal(err)
}
}
got, err := s.FindOrCreate(context.Background(), tt.args.organizationID)
if (tt.wants.err != nil) != (err != nil) { if (tt.wants.err != nil) != (err != nil) {
t.Errorf("%q. ConfigStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err) t.Errorf("%q. OrganizationConfigStore.FindOrCreate() error = %v, wantErr %v", tt.name, err, tt.wants.err)
continue continue
} }
if diff := cmp.Diff(got, tt.wants.config); diff != "" { if diff := cmp.Diff(got, tt.wants.organizationConfig); diff != "" {
t.Errorf("%q. ConfigStore.Get():\n-got/+want\ndiff %s", tt.name, diff) t.Errorf("%q. OrganizationConfigStore.FindOrCreate():\n-got/+want\ndiff %s", tt.name, diff)
} }
} }
} }
func TestConfig_Update(t *testing.T) { func TestOrganizationConfig_Update(t *testing.T) {
type args struct { type args struct {
config *chronograf.OrganizationConfig organizationConfig *chronograf.OrganizationConfig
organizationID string
} }
type wants struct { type wants struct {
config *chronograf.OrganizationConfig organizationConfig *chronograf.OrganizationConfig
err error err error
} }
tests := []struct { tests := []struct {
@ -163,9 +533,10 @@ func TestConfig_Update(t *testing.T) {
wants wants wants wants
}{ }{
{ {
name: "Set config", name: "Set default org config",
args: args{ args: args{
config: &chronograf.OrganizationConfig{ organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "default",
LogViewer: chronograf.LogViewerConfig{ LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
@ -268,9 +639,10 @@ func TestConfig_Update(t *testing.T) {
}, },
}, },
}, },
organizationID: "default",
}, },
wants: wants{ wants: wants{
config: &chronograf.OrganizationConfig{ organizationConfig: &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{ LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
@ -372,6 +744,223 @@ func TestConfig_Update(t *testing.T) {
}, },
}, },
}, },
OrganizationID: "default",
},
},
},
{
name: "Set non-default org config",
args: args{
organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "1337",
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "time",
Position: 1,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "label",
Value: "text",
},
},
},
{
Name: "timestamp",
Position: 2,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "message",
Position: 3,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "facility",
Position: 4,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "procid",
Position: 5,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Milkshake",
},
},
},
{
Name: "appname",
Position: 6,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Application",
},
},
},
{
Name: "host",
Position: 7,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
},
},
},
organizationID: "1337",
},
wants: wants{
organizationConfig: &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "time",
Position: 1,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "label",
Value: "text",
},
},
},
{
Name: "timestamp",
Position: 2,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "message",
Position: 3,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "facility",
Position: 4,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
{
Name: "procid",
Position: 5,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Milkshake",
},
},
},
{
Name: "appname",
Position: 6,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Application",
},
},
},
{
Name: "host",
Position: 7,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
},
},
},
},
OrganizationID: "1337",
}, },
}, },
}, },
@ -384,20 +973,20 @@ func TestConfig_Update(t *testing.T) {
defer client.Close() defer client.Close()
s := client.OrganizationConfigStore s := client.OrganizationConfigStore
err = s.Update(context.Background(), tt.args.config) err = s.Update(context.Background(), tt.args.organizationConfig)
if (tt.wants.err != nil) != (err != nil) { if (tt.wants.err != nil) != (err != nil) {
t.Errorf("%q. ConfigStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err) t.Errorf("%q. OrganizationConfigStore.Update() error = %v, wantErr %v", tt.name, err, tt.wants.err)
continue continue
} }
got, _ := s.Get(context.Background()) got, _ := s.FindOrCreate(context.Background(), tt.args.organizationID)
if (tt.wants.err != nil) != (err != nil) { if (tt.wants.err != nil) != (err != nil) {
t.Errorf("%q. ConfigStore.Get() error = %v, wantErr %v", tt.name, err, tt.wants.err) t.Errorf("%q. OrganizationConfigStore.Update() error = %v, wantErr %v", tt.name, err, tt.wants.err)
continue continue
} }
if diff := cmp.Diff(got, tt.wants.config); diff != "" { if diff := cmp.Diff(got, tt.wants.organizationConfig); diff != "" {
t.Errorf("%q. ConfigStore.Get():\n-got/+want\ndiff %s", tt.name, diff) t.Errorf("%q. OrganizationConfigStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
} }
} }
} }

View File

@ -38,6 +38,7 @@ const (
ErrInvalidCellOptionsText = Error("invalid text wrapping option. Valid wrappings are 'truncate', 'wrap', and 'single line'") ErrInvalidCellOptionsText = Error("invalid text wrapping option. Valid wrappings are 'truncate', 'wrap', and 'single line'")
ErrInvalidCellOptionsSort = Error("cell options sortby cannot be empty'") ErrInvalidCellOptionsSort = Error("cell options sortby cannot be empty'")
ErrInvalidCellOptionsColumns = Error("cell options columns cannot be empty'") ErrInvalidCellOptionsColumns = Error("cell options columns cannot be empty'")
ErrOrganizationConfigFindOrCreateFailed = Error("failed to find or create organization config")
) )
// Error is a domain error encountered while processing chronograf requests // Error is a domain error encountered while processing chronograf requests
@ -760,6 +761,7 @@ type ConfigStore interface {
// OrganizationConfig is the organization config for parameters that can // OrganizationConfig is the organization config for parameters that can
// be set via API, with different sections, such as LogViewer // be set via API, with different sections, such as LogViewer
type OrganizationConfig struct { type OrganizationConfig struct {
OrganizationID string `json:"organization"`
LogViewer LogViewerConfig `json:"logViewer"` LogViewer LogViewerConfig `json:"logViewer"`
} }
@ -784,12 +786,10 @@ type ColumnEncoding struct {
// OrganizationConfigStore is the storage and retrieval of organization Configs // OrganizationConfigStore is the storage and retrieval of organization Configs
type OrganizationConfigStore interface { type OrganizationConfigStore interface {
// Initialize creates the initial configuration // FindOrCreate gets an existing OrganizationConfig and creates one if none exists
Initialize(context.Context) error FindOrCreate(ctx context.Context, orgID string) (*OrganizationConfig, error)
// Get retrieves the whole Config from the OrganizationConfigStore // Update updates the whole organization config in the OrganizationConfigStore
Get(context.Context) (*Config, error) Update(context.Context, *OrganizationConfig) error
// Update updates the whole Config in the OrganizationConfigStore
Update(context.Context, *Config) error
} }
// BuildInfo is sent to the usage client to track versions and commits // BuildInfo is sent to the usage client to track versions and commits

24
mocks/org_config.go Normal file
View File

@ -0,0 +1,24 @@
package mocks
import (
"context"
"github.com/influxdata/chronograf"
)
var _ chronograf.OrganizationConfigStore = &OrganizationConfigStore{}
type OrganizationConfigStore struct {
//InitializeF func(ctx context.Context) error
//GetF func(ctx context.Context, id chronograf.OrganizationID) (*chronograf.OrganizationConfig, error)
FindOrCreateF func(ctx context.Context, id string) (*chronograf.OrganizationConfig, error)
UpdateF func(ctx context.Context, target *chronograf.OrganizationConfig) error
}
func (oc *OrganizationConfigStore) FindOrCreate(ctx context.Context, id string) (*chronograf.OrganizationConfig, error) {
return oc.FindOrCreateF(ctx, id)
}
func (oc *OrganizationConfigStore) Update(ctx context.Context, target *chronograf.OrganizationConfig) error {
return oc.UpdateF(ctx, target)
}

View File

@ -16,6 +16,7 @@ type Store struct {
DashboardsStore chronograf.DashboardsStore DashboardsStore chronograf.DashboardsStore
OrganizationsStore chronograf.OrganizationsStore OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore ConfigStore chronograf.ConfigStore
OrganizationConfigStore chronograf.OrganizationConfigStore
} }
func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore { func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
@ -48,3 +49,7 @@ func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
func (s *Store) Config(ctx context.Context) chronograf.ConfigStore { func (s *Store) Config(ctx context.Context) chronograf.ConfigStore {
return s.ConfigStore return s.ConfigStore
} }
func (s *Store) OrganizationConfig(ctx context.Context) chronograf.OrganizationConfigStore {
return s.OrganizationConfigStore
}

21
noop/org_config.go Normal file
View File

@ -0,0 +1,21 @@
package noop
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// ensure OrganizationConfigStore implements chronograf.OrganizationConfigStore
var _ chronograf.OrganizationConfigStore = &OrganizationConfigStore{}
type OrganizationConfigStore struct{}
func (s *OrganizationConfigStore) FindOrCreate(context.Context, string) (*chronograf.OrganizationConfig, error) {
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
func (s *OrganizationConfigStore) Update(context.Context, *chronograf.OrganizationConfig) error {
return fmt.Errorf("cannot update conifg")
}

View File

@ -0,0 +1,56 @@
package organizations
import (
"context"
"github.com/influxdata/chronograf"
)
// ensure that OrganizationConfig implements chronograf.OrganizationConfigStore
var _ chronograf.OrganizationConfigStore = &OrganizationConfigStore{}
// OrganizationConfigStore facade on a OrganizationConfig that filters OrganizationConfigs by organization.
type OrganizationConfigStore struct {
store chronograf.OrganizationConfigStore
organization string
}
// NewOrganizationConfigStore creates a new OrganizationConfigStore from an existing
// chronograf.OrganizationConfigStore and an organization string
func NewOrganizationConfigStore(s chronograf.OrganizationConfigStore, orgID string) *OrganizationConfigStore {
return &OrganizationConfigStore{
store: s,
organization: orgID,
}
}
// FindOrCreate gets an organization's config or creates one if none exists
func (s *OrganizationConfigStore) FindOrCreate(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
var err = validOrganization(ctx)
if err != nil {
return nil, err
}
oc, err := s.store.FindOrCreate(ctx, orgID)
if err != nil {
return nil, err
}
return oc, nil
}
// Update the OrganizationConfig in OrganizationConfigStore.
func (s *OrganizationConfigStore) Update(ctx context.Context, oc *chronograf.OrganizationConfig) error {
err := validOrganization(ctx)
if err != nil {
return err
}
_, err = s.store.FindOrCreate(ctx, oc.OrganizationID)
if err != nil {
return err
}
return s.store.Update(ctx, oc)
}

View File

@ -95,13 +95,8 @@ func AuthorizedUser(
next http.HandlerFunc, next http.HandlerFunc,
) http.HandlerFunc { ) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !useAuth {
ctx := r.Context() ctx := r.Context()
// If there is no auth, then give the user raw access to the DataStore serverCtx := serverContext(ctx)
r = r.WithContext(serverContext(ctx))
next(w, r)
return
}
log := logger. log := logger.
WithField("component", "role_auth"). WithField("component", "role_auth").
@ -109,8 +104,24 @@ func AuthorizedUser(
WithField("method", r.Method). WithField("method", r.Method).
WithField("url", r.URL) WithField("url", r.URL)
ctx := r.Context() defaultOrg, err := store.Organizations(serverCtx).DefaultOrganization(serverCtx)
serverCtx := serverContext(ctx) if err != nil {
log.Error(fmt.Sprintf("Failed to retrieve the default organization: %v", err))
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
if !useAuth {
// If there is no auth, then set the organization id to be the default org id on context
// so that calls like hasOrganizationContext as used in Organization Config service
// method OrganizationConfig can successfully get the organization id
ctx = context.WithValue(ctx, organizations.ContextKey, defaultOrg.ID)
// And if there is no auth, then give the user raw access to the DataStore
r = r.WithContext(serverContext(ctx))
next(w, r)
return
}
p, err := getValidPrincipal(ctx) p, err := getValidPrincipal(ctx)
if err != nil { if err != nil {
@ -127,12 +138,6 @@ func AuthorizedUser(
// This is as if the user was logged into the default organization // This is as if the user was logged into the default organization
if p.Organization == "" { if p.Organization == "" {
defaultOrg, err := store.Organizations(serverCtx).DefaultOrganization(serverCtx)
if err != nil {
log.Error(fmt.Sprintf("Failed to retrieve the default organization: %v", err))
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
p.Organization = defaultOrg.ID p.Organization = defaultOrg.ID
} }

View File

@ -63,7 +63,6 @@ func TestAuthorizedToken(t *testing.T) {
} }
} }
} }
func TestAuthorizedUser(t *testing.T) { func TestAuthorizedUser(t *testing.T) {
type fields struct { type fields struct {
UsersStore chronograf.UsersStore UsersStore chronograf.UsersStore
@ -102,7 +101,7 @@ func TestAuthorizedUser(t *testing.T) {
args: args{ args: args{
useAuth: false, useAuth: false,
}, },
hasOrganizationContext: false, hasOrganizationContext: true,
hasSuperAdminContext: false, hasSuperAdminContext: false,
hasRoleContext: false, hasRoleContext: false,
hasServerContext: true, hasServerContext: true,
@ -1047,6 +1046,11 @@ func TestAuthorizedUser(t *testing.T) {
Name: "The ShillBillThrilliettas", Name: "The ShillBillThrilliettas",
}, nil }, nil
}, },
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
}, nil
},
}, },
Logger: clog.New(clog.DebugLevel), Logger: clog.New(clog.DebugLevel),
}, },

View File

@ -22,17 +22,17 @@ func newOrganizationConfigResponse(config chronograf.OrganizationConfig) *organi
} }
} }
type logViewerOrganizationConfigResponse struct { type logViewerConfigResponse struct {
Links selfLinks `json:"links"` Links selfLinks `json:"links"`
chronograf.LogViewerOrganizationConfig chronograf.LogViewerConfig
} }
func newAuthConfigResponse(config chronograf.OrganizationConfig) *logViewerOrganizationConfigResponse { func newLogViewerConfigResponse(config chronograf.OrganizationConfig) *logViewerConfigResponse {
return &logViewerOrganizationConfigResponse{ return &logViewerConfigResponse{
Links: selfLinks{ Links: selfLinks{
Self: "/chronograf/v1/org_config/logviewer", Self: "/chronograf/v1/org_config/logviewer",
}, },
LogViewerOrganizationConfig: config.LogViewer, LogViewerConfig: config.LogViewer,
} }
} }
@ -40,14 +40,20 @@ func newAuthConfigResponse(config chronograf.OrganizationConfig) *logViewerOrgan
func (s *Service) OrganizationConfig(w http.ResponseWriter, r *http.Request) { func (s *Service) OrganizationConfig(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
config, err := s.Store.OrganizationConfig(ctx).Get(ctx) orgID, ok := hasOrganizationContext(ctx)
if !ok {
Error(w, http.StatusBadRequest, "Organization not found on context", s.Logger)
return
}
config, err := s.Store.OrganizationConfig(ctx).FindOrCreate(ctx, orgID)
if err != nil { if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger) Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return return
} }
if config == nil { if config == nil {
Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger) Error(w, http.StatusBadRequest, "Organization configuration object was nil", s.Logger)
return return
} }
res := newOrganizationConfigResponse(*config) res := newOrganizationConfigResponse(*config)
@ -59,14 +65,20 @@ func (s *Service) OrganizationConfig(w http.ResponseWriter, r *http.Request) {
func (s *Service) LogViewerOrganizationConfig(w http.ResponseWriter, r *http.Request) { func (s *Service) LogViewerOrganizationConfig(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
config, err := s.Store.OrganizationConfig(ctx).Get(ctx) orgID, ok := hasOrganizationContext(ctx)
if !ok {
Error(w, http.StatusBadRequest, "Organization not found on context", s.Logger)
return
}
config, err := s.Store.OrganizationConfig(ctx).FindOrCreate(ctx, orgID)
if err != nil { if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger) Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return return
} }
if config == nil { if config == nil {
Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger) Error(w, http.StatusBadRequest, "Organization configuration object was nil", s.Logger)
return return
} }
@ -79,6 +91,12 @@ func (s *Service) LogViewerOrganizationConfig(w http.ResponseWriter, r *http.Req
func (s *Service) ReplaceLogViewerOrganizationConfig(w http.ResponseWriter, r *http.Request) { func (s *Service) ReplaceLogViewerOrganizationConfig(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
orgID, ok := hasOrganizationContext(ctx)
if !ok {
Error(w, http.StatusBadRequest, "Organization not found on context", s.Logger)
return
}
var logViewerConfig chronograf.LogViewerConfig var logViewerConfig chronograf.LogViewerConfig
if err := json.NewDecoder(r.Body).Decode(&logViewerConfig); err != nil { if err := json.NewDecoder(r.Body).Decode(&logViewerConfig); err != nil {
invalidJSON(w, s.Logger) invalidJSON(w, s.Logger)
@ -89,13 +107,13 @@ func (s *Service) ReplaceLogViewerOrganizationConfig(w http.ResponseWriter, r *h
return return
} }
config, err := s.Store.OrganizationConfig(ctx).Get(ctx) config, err := s.Store.OrganizationConfig(ctx).FindOrCreate(ctx, orgID)
if err != nil { if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger) Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return return
} }
if config == nil { if config == nil {
Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger) Error(w, http.StatusBadRequest, "Organization configuration object was nil", s.Logger)
return return
} }
config.LogViewer = logViewerConfig config.LogViewer = logViewerConfig

View File

@ -2,6 +2,7 @@ package server
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"net/http/httptest" "net/http/httptest"
@ -10,11 +11,15 @@ import (
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/log" "github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/mocks" "github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/organizations"
) )
func TestOrganizationConfig(t *testing.T) { func TestOrganizationConfig(t *testing.T) {
type args struct {
organizationID string
}
type fields struct { type fields struct {
OrganizationConfigStore chronograf.OrganizationConfigStore organizationConfigStore chronograf.OrganizationConfigStore
} }
type wants struct { type wants struct {
statusCode int statusCode int
@ -24,15 +29,23 @@ func TestOrganizationConfig(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args args
fields fields fields fields
wants wants wants wants
}{ }{
{ {
name: "Get organization configuration settings", name: "Get organization configuration",
args: args{
organizationID: "default",
},
fields: fields{ fields: fields{
OrganizationConfigStore: &mocks.OrganizationConfigStore{ organizationConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{ FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
LogViewer: chronograf.LogViewer{ switch orgID {
case "default":
return &chronograf.OrganizationConfig{
OrganizationID: "default",
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
Name: "time", Name: "time",
@ -137,13 +150,17 @@ func TestOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
}, },
}, },
}, },
wants: wants{ wants: wants{
statusCode: 200, statusCode: 200,
contentType: "application/json", contentType: "application/json",
body: `{"links":{"self":"/chronograf/v1/config"},"auth":{"superAdminNewUsers":false}}`, body: `{"links":{"self":"/chronograf/v1/org_config"},"organization":"default","logViewer":{"columns":[{"name":"time","position":0,"encodings":[{"type":"visibility","value":"hidden"}]},{"name":"severity","position":1,"encodings":[{"type":"visibility","value":"visible"},{"type":"label","value":"icon"},{"type":"label","value":"text"}]},{"name":"timestamp","position":2,"encodings":[{"type":"visibility","value":"visible"}]},{"name":"message","position":3,"encodings":[{"type":"visibility","value":"visible"}]},{"name":"facility","position":4,"encodings":[{"type":"visibility","value":"visible"}]},{"name":"procid","position":5,"encodings":[{"type":"visibility","value":"visible"},{"type":"displayName","value":"Proc ID"}]},{"name":"appname","position":6,"encodings":[{"type":"visibility","value":"visible"},{"type":"displayName","value":"Application"}]},{"name":"host","position":7,"encodings":[{"type":"visibility","value":"visible"}]}]}}`,
}, },
}, },
} }
@ -152,36 +169,41 @@ func TestOrganizationConfig(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := &Service{ s := &Service{
Store: &mocks.Store{ Store: &mocks.Store{
OrganizationConfigStore: tt.fields.OrganizationConfigStore, OrganizationConfigStore: tt.fields.organizationConfigStore,
}, },
Logger: log.New(log.DebugLevel), Logger: log.New(log.DebugLevel),
} }
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "http://any.url", nil) r := httptest.NewRequest("GET", "http://any.url", nil)
ctx := context.WithValue(r.Context(), organizations.ContextKey, tt.args.organizationID)
r = r.WithContext(ctx)
s.Config(w, r) s.OrganizationConfig(w, r)
resp := w.Result() resp := w.Result()
content := resp.Header.Get("Content-Type") content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body) body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wants.statusCode { if resp.StatusCode != tt.wants.statusCode {
t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) t.Errorf("%q. OrganizationConfig() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode)
} }
if tt.wants.contentType != "" && content != tt.wants.contentType { if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType) t.Errorf("%q. OrganizationConfig() = %v, want %v", tt.name, content, tt.wants.contentType)
} }
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { 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) t.Errorf("%q. OrganizationConfig() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
} }
}) })
} }
} }
func TestLogViewerOrganizationConfig(t *testing.T) { func TestLogViewerOrganizationConfig(t *testing.T) {
type args struct {
organizationID string
}
type fields struct { type fields struct {
OrganizationConfigStore chronograf.OrganizationConfigStore organizationConfigStore chronograf.OrganizationConfigStore
} }
type wants struct { type wants struct {
statusCode int statusCode int
@ -191,14 +213,21 @@ func TestLogViewerOrganizationConfig(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args args
fields fields fields fields
wants wants wants wants
}{ }{
{ {
name: "Get log viewer configuration", name: "Get log viewer configuration",
args: args{
organizationID: "default",
},
fields: fields{ fields: fields{
OrganizationConfigStore: &mocks.OrganizationConfigStore{ organizationConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{ FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "default":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{ LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
@ -223,13 +252,17 @@ func TestLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
}, },
}, },
}, },
wants: wants{ wants: wants{
statusCode: 200, statusCode: 200,
contentType: "application/json", contentType: "application/json",
body: `{"links":{"self":"/chronograf/v1/config/logviewer"},"columns":[{"name":"severity","position":0,"encodings":[{"type":"color","value":"emergency","name":"ruby"},{"type":"color","value":"info","name":"rainforest"},{"type":"displayName","value":"Log Severity"}]}]}`, body: `{"links":{"self":"/chronograf/v1/org_config/logviewer"},"columns":[{"name":"severity","position":0,"encodings":[{"type":"color","value":"emergency","name":"ruby"},{"type":"color","value":"info","name":"rainforest"},{"type":"displayName","value":"Log Severity"}]}]}`,
}, },
}, },
} }
@ -238,13 +271,15 @@ func TestLogViewerOrganizationConfig(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := &Service{ s := &Service{
Store: &mocks.Store{ Store: &mocks.Store{
ConfigStore: tt.fields.ConfigStore, OrganizationConfigStore: tt.fields.organizationConfigStore,
}, },
Logger: log.New(log.DebugLevel), Logger: log.New(log.DebugLevel),
} }
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "http://any.url", nil) r := httptest.NewRequest("GET", "http://any.url", nil)
ctx := context.WithValue(r.Context(), organizations.ContextKey, tt.args.organizationID)
r = r.WithContext(ctx)
s.LogViewerOrganizationConfig(w, r) s.LogViewerOrganizationConfig(w, r)
@ -267,10 +302,11 @@ func TestLogViewerOrganizationConfig(t *testing.T) {
func TestReplaceLogViewerOrganizationConfig(t *testing.T) { func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
type fields struct { type fields struct {
ConfigStore chronograf.OrganizationConfigStore organizationConfigStore chronograf.OrganizationConfigStore
} }
type args struct { type args struct {
payload interface{} // expects JSON serializable struct payload interface{} // expects JSON serializable struct
organizationID string
} }
type wants struct { type wants struct {
statusCode int statusCode int
@ -287,8 +323,11 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
{ {
name: "Set log viewer configuration", name: "Set log viewer configuration",
fields: fields{ fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{ organizationConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{ FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{ LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
@ -312,6 +351,13 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
},
UpdateF: func(ctx context.Context, target *chronograf.OrganizationConfig) error {
return nil
}, },
}, },
}, },
@ -358,18 +404,22 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
organizationID: "1337",
}, },
wants: wants{ wants: wants{
statusCode: 200, statusCode: 200,
contentType: "application/json", contentType: "application/json",
body: `{"links":{"self":"/chronograf/v1/config/logviewer"},"columns":[{"name":"severity","position":1,"encodings":[{"type":"color","value":"info","name":"pineapple"},{"type":"color","value":"emergency","name":"ruby"},{"type":"visibility","value":"visible"},{"type":"label","value":"icon"}]},{"name":"messages","position":0,"encodings":[{"type":"displayName","value":"Log Messages"},{"type":"visibility","value":"visible"}]}]}`, body: `{"links":{"self":"/chronograf/v1/org_config/logviewer"},"columns":[{"name":"severity","position":1,"encodings":[{"type":"color","value":"info","name":"pineapple"},{"type":"color","value":"emergency","name":"ruby"},{"type":"visibility","value":"visible"},{"type":"label","value":"icon"}]},{"name":"messages","position":0,"encodings":[{"type":"displayName","value":"Log Messages"},{"type":"visibility","value":"visible"}]}]}`,
}, },
}, },
{ {
name: "Set invalid log viewer configuration empty", name: "Set invalid log viewer configuration empty",
fields: fields{ fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{ organizationConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{ FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{ LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
@ -393,6 +443,13 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
},
UpdateF: func(ctx context.Context, target *chronograf.OrganizationConfig) error {
return nil
}, },
}, },
}, },
@ -400,6 +457,7 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
payload: chronograf.LogViewerConfig{ payload: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{}, Columns: []chronograf.LogViewerColumn{},
}, },
organizationID: "1337",
}, },
wants: wants{ wants: wants{
statusCode: 400, statusCode: 400,
@ -410,8 +468,11 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
{ {
name: "Set invalid log viewer configuration - duplicate column name", name: "Set invalid log viewer configuration - duplicate column name",
fields: fields{ fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{ organizationConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{ FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{ LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
@ -426,6 +487,13 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
},
UpdateF: func(ctx context.Context, target *chronograf.OrganizationConfig) error {
return nil
}, },
}, },
}, },
@ -454,6 +522,7 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
organizationID: "1337",
}, },
wants: wants{ wants: wants{
statusCode: 400, statusCode: 400,
@ -464,8 +533,11 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
{ {
name: "Set invalid log viewer configuration - multiple columns with same position value", name: "Set invalid log viewer configuration - multiple columns with same position value",
fields: fields{ fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{ organizationConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{ FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{ LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
@ -480,6 +552,13 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
},
UpdateF: func(ctx context.Context, target *chronograf.OrganizationConfig) error {
return nil
}, },
}, },
}, },
@ -508,6 +587,7 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
organizationID: "1337",
}, },
wants: wants{ wants: wants{
statusCode: 400, statusCode: 400,
@ -518,8 +598,11 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
{ {
name: "Set invalid log viewer configuration no visibility", name: "Set invalid log viewer configuration no visibility",
fields: fields{ fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{ organizationConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{ FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{ LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{ Columns: []chronograf.LogViewerColumn{
{ {
@ -539,6 +622,13 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
},
UpdateF: func(ctx context.Context, target *chronograf.OrganizationConfig) error {
return nil
}, },
}, },
}, },
@ -567,6 +657,7 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
}, },
}, },
}, },
organizationID: "1337",
}, },
wants: wants{ wants: wants{
statusCode: 400, statusCode: 400,
@ -580,13 +671,15 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := &Service{ s := &Service{
Store: &mocks.Store{ Store: &mocks.Store{
OrganizationConfigStore: tt.fields.OrganizationConfigStore, OrganizationConfigStore: tt.fields.organizationConfigStore,
}, },
Logger: log.New(log.DebugLevel), Logger: log.New(log.DebugLevel),
} }
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "http://any.url", nil) r := httptest.NewRequest("GET", "http://any.url", nil)
ctx := context.WithValue(r.Context(), organizations.ContextKey, tt.args.organizationID)
r = r.WithContext(ctx)
buf, _ := json.Marshal(tt.args.payload) buf, _ := json.Marshal(tt.args.payload)
r.Body = ioutil.NopCloser(bytes.NewReader(buf)) r.Body = ioutil.NopCloser(bytes.NewReader(buf))

View File

@ -494,6 +494,7 @@ func openService(ctx context.Context, buildInfo chronograf.BuildInfo, boltPath s
UsersStore: db.UsersStore, UsersStore: db.UsersStore,
ConfigStore: db.ConfigStore, ConfigStore: db.ConfigStore,
MappingsStore: db.MappingsStore, MappingsStore: db.MappingsStore,
OrganizationConfigStore: db.OrganizationConfigStore,
}, },
Logger: logger, Logger: logger,
UseAuth: useAuth, UseAuth: useAuth,

View File

@ -91,6 +91,7 @@ type DataStore interface {
Mappings(ctx context.Context) chronograf.MappingsStore Mappings(ctx context.Context) chronograf.MappingsStore
Dashboards(ctx context.Context) chronograf.DashboardsStore Dashboards(ctx context.Context) chronograf.DashboardsStore
Config(ctx context.Context) chronograf.ConfigStore Config(ctx context.Context) chronograf.ConfigStore
OrganizationConfig(ctx context.Context) chronograf.OrganizationConfigStore
} }
// ensure that Store implements a DataStore // ensure that Store implements a DataStore
@ -106,10 +107,11 @@ type Store struct {
MappingsStore chronograf.MappingsStore MappingsStore chronograf.MappingsStore
OrganizationsStore chronograf.OrganizationsStore OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore ConfigStore chronograf.ConfigStore
OrganizationConfigStore chronograf.OrganizationConfigStore
} }
// Sources returns a noop.SourcesStore if the context has no organization specified // Sources returns a noop.SourcesStore if the context has no organization specified
// and a organization.SourcesStore otherwise. // and an organization.SourcesStore otherwise.
func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore { func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
if isServer := hasServerContext(ctx); isServer { if isServer := hasServerContext(ctx); isServer {
return s.SourcesStore return s.SourcesStore
@ -122,7 +124,7 @@ func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
} }
// Servers returns a noop.ServersStore if the context has no organization specified // Servers returns a noop.ServersStore if the context has no organization specified
// and a organization.ServersStore otherwise. // and an organization.ServersStore otherwise.
func (s *Store) Servers(ctx context.Context) chronograf.ServersStore { func (s *Store) Servers(ctx context.Context) chronograf.ServersStore {
if isServer := hasServerContext(ctx); isServer { if isServer := hasServerContext(ctx); isServer {
return s.ServersStore return s.ServersStore
@ -157,7 +159,7 @@ func (s *Store) Users(ctx context.Context) chronograf.UsersStore {
} }
// Dashboards returns a noop.DashboardsStore if the context has no organization specified // Dashboards returns a noop.DashboardsStore if the context has no organization specified
// and a organization.DashboardsStore otherwise. // and an organization.DashboardsStore otherwise.
func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore { func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
if isServer := hasServerContext(ctx); isServer { if isServer := hasServerContext(ctx); isServer {
return s.DashboardsStore return s.DashboardsStore
@ -169,17 +171,17 @@ func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
return &noop.DashboardsStore{} return &noop.DashboardsStore{}
} }
// Settings returns a noop.SettingsStore if the context has no organization specified // OrganizationConfig returns a noop.OrganizationConfigStore if the context has no organization specified
// and a organization.SettingsStore otherwise. // and an organization.OrganizationConfigStore otherwise.
func (s *Store) Settings(ctx context.Context) chronograf.SettingsStore { func (s *Store) OrganizationConfig(ctx context.Context) chronograf.OrganizationConfigStore {
if isServer := hasServerContext(ctx); isServer { if isServer := hasServerContext(ctx); isServer {
return s.SettingsStore return s.OrganizationConfigStore
} }
if org, ok := hasOrganizationContext(ctx); ok { if orgID, ok := hasOrganizationContext(ctx); ok {
return organizations.NewSettingsStore(s.SettingsStore, org) return organizations.NewOrganizationConfigStore(s.OrganizationConfigStore, orgID)
} }
return &noop.SettingsStore{} return &noop.OrganizationConfigStore{}
} }
// Organizations returns the underlying OrganizationsStore. // Organizations returns the underlying OrganizationsStore.