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/3806/head
Jared Scheib 2018-07-05 18:40:52 -07:00
parent ecb5347597
commit ae475c7ae8
17 changed files with 2334 additions and 627 deletions

View File

@ -33,15 +33,16 @@ type Client struct {
Now func() time.Time
LayoutIDs chronograf.ID
BuildStore *BuildStore
SourcesStore *SourcesStore
ServersStore *ServersStore
LayoutsStore *LayoutsStore
DashboardsStore *DashboardsStore
UsersStore *UsersStore
OrganizationsStore *OrganizationsStore
ConfigStore *ConfigStore
MappingsStore *MappingsStore
BuildStore *BuildStore
SourcesStore *SourcesStore
ServersStore *ServersStore
LayoutsStore *LayoutsStore
DashboardsStore *DashboardsStore
UsersStore *UsersStore
OrganizationsStore *OrganizationsStore
ConfigStore *ConfigStore
MappingsStore *MappingsStore
OrganizationConfigStore *OrganizationConfigStore
}
// NewClient initializes all stores
@ -62,6 +63,7 @@ func NewClient() *Client {
c.OrganizationsStore = &OrganizationsStore{client: c}
c.ConfigStore = &ConfigStore{client: c}
c.MappingsStore = &MappingsStore{client: c}
c.OrganizationConfigStore = &OrganizationConfigStore{client: c}
return c
}
@ -161,6 +163,10 @@ func (c *Client) initialize(ctx context.Context) error {
if _, err := tx.CreateBucketIfNotExists(MappingsBucket); err != nil {
return err
}
// Always create OrganizationConfig bucket.
if _, err := tx.CreateBucketIfNotExists(OrganizationConfigBucket); err != nil {
return err
}
return nil
}); err != nil {
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 {
return err
}
if err := c.OrganizationConfigStore.Migrate(ctx); err != nil {
return err
}
MigrateAll(c)
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -215,7 +215,8 @@ message AuthConfig {
}
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 {

View File

@ -12,145 +12,26 @@ import (
// Ensure OrganizationConfigStore implements chronograf.OrganizationConfigStore.
var _ chronograf.OrganizationConfigStore = &OrganizationConfigStore{}
// ConfigBucket is used to store chronograf application state
var ConfigBucket = []byte("ConfigV1")
// OrganizationConfigBucket is used to store chronograf organization configurations
var OrganizationConfigBucket = []byte("OrganizationConfigV1")
// configID is the boltDB key where the configuration object is stored
var configID = []byte("config/v1")
// OrganizationConfigStore uses bolt to store and retrieve global
// application configuration
// OrganizationConfigStore uses bolt to store and retrieve organization configurations
type OrganizationConfigStore struct {
client *Client
}
func (s *OrganizationConfigStore) Migrate(ctx context.Context) error {
if _, err := s.Get(ctx); err != nil {
return s.Initialize(ctx)
}
return nil
}
func (s *OrganizationConfigStore) Initialize(ctx context.Context) error {
cfg := chronograf.OrganizationConfig{
OrganizationConfig: chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewer{
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",
},
},
},
},
},
},
}
return s.Update(ctx, &cfg)
}
func (s *OrganizationConfigStore) Get(ctx context.Context) (*chronograf.OrganizationConfig, error) {
// FindOrCreate gets an OrganizationConfig from the store or creates one if none exists for this organization
func (s *OrganizationConfigStore) FindOrCreate(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
var cfg chronograf.OrganizationConfig
err := s.client.db.View(func(tx *bolt.Tx) error {
v := tx.Bucket(ConfigBucket).Get(configID)
v := tx.Bucket(OrganizationConfigBucket).Get([]byte(orgID))
if v == nil {
return chronograf.ErrConfigNotFound
cfg = newOrganizationConfig(orgID)
return nil
}
return internal.UnmarshalOrganizationConfig(v, &cfg)
})
@ -161,6 +42,7 @@ func (s *OrganizationConfigStore) Get(ctx context.Context) (*chronograf.Organiza
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")
@ -168,9 +50,120 @@ func (s *OrganizationConfigStore) Update(ctx context.Context, cfg *chronograf.Or
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 {
} 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{
{
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",
},
},
},
},
},
}
}

View File

@ -8,19 +8,29 @@ import (
"github.com/influxdata/chronograf"
)
func TestConfig_Get(t *testing.T) {
func TestOrganizationConfig_FindOrCreate(t *testing.T) {
type args struct {
organizationID string
}
type wants struct {
config *chronograf.OrganizationConfig
err error
organizationConfig *chronograf.OrganizationConfig
err error
}
tests := []struct {
name string
wants wants
name string
args args
addFirst bool
wants wants
}{
{
name: "Get config",
name: "Get non-existent default config from default org",
args: args{
organizationID: "default",
},
addFirst: false,
wants: wants{
config: &chronograf.OrganizationConfig{
organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "default",
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
@ -129,64 +139,40 @@ func TestConfig_Get(t *testing.T) {
},
},
},
}
for _, tt := range tests {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.OrganizationConfigStore
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.OrganizationConfig
}
type wants struct {
config *chronograf.OrganizationConfig
err error
}
tests := []struct {
name string
args args
wants wants
}{
{
name: "Set config",
name: "Get non-existent default config from non-default org",
args: args{
config: &chronograf.OrganizationConfig{
organizationID: "1",
},
addFirst: false,
wants: wants{
organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "1",
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "time",
Position: 1,
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
Value: "hidden",
},
},
},
{
Name: "severity",
Position: 0,
Position: 1,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
},
{
Type: "label",
Value: "icon",
},
{
Type: "label",
Value: "text",
@ -237,7 +223,7 @@ func TestConfig_Update(t *testing.T) {
},
{
Type: "displayName",
Value: "Milkshake",
Value: "Proc ID",
},
},
},
@ -269,8 +255,16 @@ func TestConfig_Update(t *testing.T) {
},
},
},
},
{
name: "Get existing/modified config from default org",
args: args{
organizationID: "default",
},
addFirst: true,
wants: wants{
config: &chronograf.OrganizationConfig{
organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "default",
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
@ -279,7 +273,7 @@ func TestConfig_Update(t *testing.T) {
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
Value: "hidden",
},
},
},
@ -290,7 +284,11 @@ func TestConfig_Update(t *testing.T) {
{
Type: "visibility",
Value: "visible",
Value: "hidden",
},
{
Type: "label",
Value: "icon",
},
{
Type: "label",
@ -342,7 +340,124 @@ func TestConfig_Update(t *testing.T) {
},
{
Type: "displayName",
Value: "Milkshake",
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",
},
},
},
@ -384,20 +499,494 @@ func TestConfig_Update(t *testing.T) {
defer client.Close()
s := client.OrganizationConfigStore
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
if tt.addFirst {
if err := s.Update(context.Background(), tt.wants.organizationConfig); err != nil {
t.Fatal(err)
}
}
got, _ := s.Get(context.Background())
got, err := s.FindOrCreate(context.Background(), tt.args.organizationID)
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
}
if diff := cmp.Diff(got, tt.wants.config); diff != "" {
t.Errorf("%q. ConfigStore.Get():\n-got/+want\ndiff %s", tt.name, diff)
if diff := cmp.Diff(got, tt.wants.organizationConfig); diff != "" {
t.Errorf("%q. OrganizationConfigStore.FindOrCreate():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}
func TestOrganizationConfig_Update(t *testing.T) {
type args struct {
organizationConfig *chronograf.OrganizationConfig
organizationID string
}
type wants struct {
organizationConfig *chronograf.OrganizationConfig
err error
}
tests := []struct {
name string
args args
wants wants
}{
{
name: "Set default org config",
args: args{
organizationConfig: &chronograf.OrganizationConfig{
OrganizationID: "default",
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: "default",
},
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: "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",
},
},
},
}
for _, tt := range tests {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.OrganizationConfigStore
err = s.Update(context.Background(), tt.args.organizationConfig)
if (tt.wants.err != nil) != (err != nil) {
t.Errorf("%q. OrganizationConfigStore.Update() error = %v, wantErr %v", tt.name, err, tt.wants.err)
continue
}
got, _ := s.FindOrCreate(context.Background(), tt.args.organizationID)
if (tt.wants.err != nil) != (err != nil) {
t.Errorf("%q. OrganizationConfigStore.Update() error = %v, wantErr %v", tt.name, err, tt.wants.err)
continue
}
if diff := cmp.Diff(got, tt.wants.organizationConfig); diff != "" {
t.Errorf("%q. OrganizationConfigStore.Update():\n-got/+want\ndiff %s", tt.name, diff)
}
}
}

View File

@ -9,35 +9,36 @@ import (
// General errors.
const (
ErrUpstreamTimeout = Error("request to backend timed out")
ErrSourceNotFound = Error("source not found")
ErrServerNotFound = Error("server not found")
ErrLayoutNotFound = Error("layout not found")
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', 'text', and 'background'")
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
ErrInvalidLegend = Error("Invalid legend. Both type and orientation must be set")
ErrInvalidLegendType = Error("Invalid legend type. Valid legend type is 'static'")
ErrInvalidLegendOrient = Error("Invalid orientation type. Valid orientation types are 'top', 'bottom', 'right', 'left'")
ErrUserAlreadyExists = Error("user already exists")
ErrOrganizationNotFound = Error("organization not found")
ErrMappingNotFound = Error("mapping not found")
ErrOrganizationAlreadyExists = Error("organization already exists")
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
ErrConfigNotFound = Error("cannot find configuration")
ErrAnnotationNotFound = Error("annotation not found")
ErrInvalidCellOptionsText = Error("invalid text wrapping option. Valid wrappings are 'truncate', 'wrap', and 'single line'")
ErrInvalidCellOptionsSort = Error("cell options sortby cannot be empty'")
ErrInvalidCellOptionsColumns = Error("cell options columns cannot be empty'")
ErrUpstreamTimeout = Error("request to backend timed out")
ErrSourceNotFound = Error("source not found")
ErrServerNotFound = Error("server not found")
ErrLayoutNotFound = Error("layout not found")
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', 'text', and 'background'")
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
ErrInvalidLegend = Error("Invalid legend. Both type and orientation must be set")
ErrInvalidLegendType = Error("Invalid legend type. Valid legend type is 'static'")
ErrInvalidLegendOrient = Error("Invalid orientation type. Valid orientation types are 'top', 'bottom', 'right', 'left'")
ErrUserAlreadyExists = Error("user already exists")
ErrOrganizationNotFound = Error("organization not found")
ErrMappingNotFound = Error("mapping not found")
ErrOrganizationAlreadyExists = Error("organization already exists")
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
ErrConfigNotFound = Error("cannot find configuration")
ErrAnnotationNotFound = Error("annotation not found")
ErrInvalidCellOptionsText = Error("invalid text wrapping option. Valid wrappings are 'truncate', 'wrap', and 'single line'")
ErrInvalidCellOptionsSort = Error("cell options sortby 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
@ -760,7 +761,8 @@ type ConfigStore interface {
// OrganizationConfig is the organization config for parameters that can
// be set via API, with different sections, such as LogViewer
type OrganizationConfig struct {
LogViewer LogViewerConfig `json:"logViewer"`
OrganizationID string `json:"organization"`
LogViewer LogViewerConfig `json:"logViewer"`
}
// LogViewerConfig is the configuration settings for the Log Viewer UI
@ -784,12 +786,10 @@ type ColumnEncoding struct {
// OrganizationConfigStore is the storage and retrieval of organization Configs
type OrganizationConfigStore interface {
// Initialize creates the initial configuration
Initialize(context.Context) error
// Get retrieves the whole Config from the OrganizationConfigStore
Get(context.Context) (*Config, error)
// Update updates the whole Config in the OrganizationConfigStore
Update(context.Context, *Config) error
// FindOrCreate gets an existing OrganizationConfig and creates one if none exists
FindOrCreate(ctx context.Context, orgID string) (*OrganizationConfig, error)
// Update updates the whole organization config in the OrganizationConfigStore
Update(context.Context, *OrganizationConfig) error
}
// 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

@ -8,14 +8,15 @@ import (
// Store is a server.DataStore
type Store struct {
SourcesStore chronograf.SourcesStore
MappingsStore chronograf.MappingsStore
ServersStore chronograf.ServersStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore
SourcesStore chronograf.SourcesStore
MappingsStore chronograf.MappingsStore
ServersStore chronograf.ServersStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore
OrganizationConfigStore chronograf.OrganizationConfigStore
}
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 {
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,
) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !useAuth {
ctx := r.Context()
// If there is no auth, then give the user raw access to the DataStore
r = r.WithContext(serverContext(ctx))
next(w, r)
return
}
ctx := r.Context()
serverCtx := serverContext(ctx)
log := logger.
WithField("component", "role_auth").
@ -109,8 +104,24 @@ func AuthorizedUser(
WithField("method", r.Method).
WithField("url", r.URL)
ctx := r.Context()
serverCtx := serverContext(ctx)
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
}
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)
if err != nil {
@ -127,12 +138,6 @@ func AuthorizedUser(
// This is as if the user was logged into the default 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
}

View File

@ -63,7 +63,6 @@ func TestAuthorizedToken(t *testing.T) {
}
}
}
func TestAuthorizedUser(t *testing.T) {
type fields struct {
UsersStore chronograf.UsersStore
@ -102,7 +101,7 @@ func TestAuthorizedUser(t *testing.T) {
args: args{
useAuth: false,
},
hasOrganizationContext: false,
hasOrganizationContext: true,
hasSuperAdminContext: false,
hasRoleContext: false,
hasServerContext: true,
@ -1047,6 +1046,11 @@ func TestAuthorizedUser(t *testing.T) {
Name: "The ShillBillThrilliettas",
}, nil
},
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
}, nil
},
},
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"`
chronograf.LogViewerOrganizationConfig
chronograf.LogViewerConfig
}
func newAuthConfigResponse(config chronograf.OrganizationConfig) *logViewerOrganizationConfigResponse {
return &logViewerOrganizationConfigResponse{
func newLogViewerConfigResponse(config chronograf.OrganizationConfig) *logViewerConfigResponse {
return &logViewerConfigResponse{
Links: selfLinks{
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) {
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 {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
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
}
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) {
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 {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
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
}
@ -79,6 +91,12 @@ func (s *Service) LogViewerOrganizationConfig(w http.ResponseWriter, r *http.Req
func (s *Service) ReplaceLogViewerOrganizationConfig(w http.ResponseWriter, r *http.Request) {
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
if err := json.NewDecoder(r.Body).Decode(&logViewerConfig); err != nil {
invalidJSON(w, s.Logger)
@ -89,13 +107,13 @@ func (s *Service) ReplaceLogViewerOrganizationConfig(w http.ResponseWriter, r *h
return
}
config, err := s.Store.OrganizationConfig(ctx).Get(ctx)
config, err := s.Store.OrganizationConfig(ctx).FindOrCreate(ctx, orgID)
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)
Error(w, http.StatusBadRequest, "Organization configuration object was nil", s.Logger)
return
}
config.LogViewer = logViewerConfig

View File

@ -2,6 +2,7 @@ package server
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http/httptest"
@ -10,11 +11,15 @@ import (
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/organizations"
)
func TestOrganizationConfig(t *testing.T) {
type args struct {
organizationID string
}
type fields struct {
OrganizationConfigStore chronograf.OrganizationConfigStore
organizationConfigStore chronograf.OrganizationConfigStore
}
type wants struct {
statusCode int
@ -24,126 +29,138 @@ func TestOrganizationConfig(t *testing.T) {
tests := []struct {
name string
args args
fields fields
wants wants
}{
{
name: "Get organization configuration settings",
name: "Get organization configuration",
args: args{
organizationID: "default",
},
fields: fields{
OrganizationConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewer{
Columns: []chronograf.LogViewerColumn{
{
Name: "time",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
organizationConfigStore: &mocks.OrganizationConfigStore{
FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "default":
return &chronograf.OrganizationConfig{
OrganizationID: "default",
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Type: "visibility",
Value: "hidden",
Name: "time",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "hidden",
},
},
},
},
},
{
Name: "severity",
Position: 1,
Encodings: []chronograf.ColumnEncoding{
{
Name: "severity",
Position: 1,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
{
Type: "visibility",
Value: "visible",
},
{
Type: "label",
Value: "icon",
},
{
Type: "label",
Value: "text",
},
},
},
{
Type: "label",
Value: "icon",
},
{
Type: "label",
Value: "text",
},
},
},
{
Name: "timestamp",
Position: 2,
Encodings: []chronograf.ColumnEncoding{
Name: "timestamp",
Position: 2,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
{
Type: "visibility",
Value: "visible",
},
},
},
},
},
{
Name: "message",
Position: 3,
Encodings: []chronograf.ColumnEncoding{
{
Name: "message",
Position: 3,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
{
Type: "visibility",
Value: "visible",
},
},
},
},
},
{
Name: "facility",
Position: 4,
Encodings: []chronograf.ColumnEncoding{
{
Name: "facility",
Position: 4,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
{
Type: "visibility",
Value: "visible",
},
},
},
},
},
{
Name: "procid",
Position: 5,
Encodings: []chronograf.ColumnEncoding{
{
Name: "procid",
Position: 5,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "visible",
{
Type: "visibility",
Value: "visible",
},
{
Type: "displayName",
Value: "Proc ID",
},
},
},
{
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: "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",
},
},
},
},
},
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
},
},
},
wants: wants{
statusCode: 200,
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) {
s := &Service{
Store: &mocks.Store{
OrganizationConfigStore: tt.fields.OrganizationConfigStore,
OrganizationConfigStore: tt.fields.organizationConfigStore,
},
Logger: log.New(log.DebugLevel),
}
w := httptest.NewRecorder()
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()
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)
t.Errorf("%q. OrganizationConfig() = %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)
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 {
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) {
type args struct {
organizationID string
}
type fields struct {
OrganizationConfigStore chronograf.OrganizationConfigStore
organizationConfigStore chronograf.OrganizationConfigStore
}
type wants struct {
statusCode int
@ -191,45 +213,56 @@ func TestLogViewerOrganizationConfig(t *testing.T) {
tests := []struct {
name string
args args
fields fields
wants wants
}{
{
name: "Get log viewer configuration",
args: args{
organizationID: "default",
},
fields: fields{
OrganizationConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
organizationConfigStore: &mocks.OrganizationConfigStore{
FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "default":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Type: "color",
Value: "emergency",
Name: "ruby",
},
{
Type: "color",
Value: "info",
Name: "rainforest",
},
{
Type: "displayName",
Value: "Log Severity",
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "color",
Value: "emergency",
Name: "ruby",
},
{
Type: "color",
Value: "info",
Name: "rainforest",
},
{
Type: "displayName",
Value: "Log Severity",
},
},
},
},
},
},
},
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
},
},
},
wants: wants{
statusCode: 200,
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) {
s := &Service{
Store: &mocks.Store{
ConfigStore: tt.fields.ConfigStore,
OrganizationConfigStore: tt.fields.organizationConfigStore,
},
Logger: log.New(log.DebugLevel),
}
w := httptest.NewRecorder()
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)
@ -267,10 +302,11 @@ func TestLogViewerOrganizationConfig(t *testing.T) {
func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
type fields struct {
ConfigStore chronograf.OrganizationConfigStore
organizationConfigStore chronograf.OrganizationConfigStore
}
type args struct {
payload interface{} // expects JSON serializable struct
payload interface{} // expects JSON serializable struct
organizationID string
}
type wants struct {
statusCode int
@ -287,31 +323,41 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
{
name: "Set log viewer configuration",
fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
organizationConfigStore: &mocks.OrganizationConfigStore{
FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Type: "color",
Value: "info",
Name: "rainforest",
},
{
Type: "visibility",
Value: "visible",
},
{
Type: "label",
Value: "icon",
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "color",
Value: "info",
Name: "rainforest",
},
{
Type: "visibility",
Value: "visible",
},
{
Type: "label",
Value: "icon",
},
},
},
},
},
},
},
}, nil
default:
return nil, chronograf.ErrOrganizationConfigFindOrCreateFailed
}
},
UpdateF: func(ctx context.Context, target *chronograf.OrganizationConfig) error {
return nil
},
},
},
@ -358,41 +404,52 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
},
},
},
organizationID: "1337",
},
wants: wants{
statusCode: 200,
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",
fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
organizationConfigStore: &mocks.OrganizationConfigStore{
FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Type: "color",
Value: "info",
Name: "rainforest",
},
{
Type: "label",
Value: "icon",
},
{
Type: "visibility",
Value: "visible",
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "color",
Value: "info",
Name: "rainforest",
},
{
Type: "label",
Value: "icon",
},
{
Type: "visibility",
Value: "visible",
},
},
},
},
},
},
},
}, 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{
Columns: []chronograf.LogViewerColumn{},
},
organizationID: "1337",
},
wants: wants{
statusCode: 400,
@ -410,22 +468,32 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
{
name: "Set invalid log viewer configuration - duplicate column name",
fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "procid",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
organizationConfigStore: &mocks.OrganizationConfigStore{
FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Type: "visibility",
Value: "hidden",
Name: "procid",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "hidden",
},
},
},
},
},
},
},
}, 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{
statusCode: 400,
@ -464,22 +533,32 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
{
name: "Set invalid log viewer configuration - multiple columns with same position value",
fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "procid",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
organizationConfigStore: &mocks.OrganizationConfigStore{
FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Type: "visibility",
Value: "hidden",
Name: "procid",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "visibility",
Value: "hidden",
},
},
},
},
},
},
},
}, 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{
statusCode: 400,
@ -518,27 +598,37 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
{
name: "Set invalid log viewer configuration no visibility",
fields: fields{
ConfigStore: &mocks.OrganizationConfigStore{
OrganizationConfig: &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
organizationConfigStore: &mocks.OrganizationConfigStore{
FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) {
switch orgID {
case "1337":
return &chronograf.OrganizationConfig{
LogViewer: chronograf.LogViewerConfig{
Columns: []chronograf.LogViewerColumn{
{
Type: "color",
Value: "info",
Name: "rainforest",
},
{
Type: "label",
Value: "icon",
Name: "severity",
Position: 0,
Encodings: []chronograf.ColumnEncoding{
{
Type: "color",
Value: "info",
Name: "rainforest",
},
{
Type: "label",
Value: "icon",
},
},
},
},
},
},
},
}, 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{
statusCode: 400,
@ -580,13 +671,15 @@ func TestReplaceLogViewerOrganizationConfig(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
OrganizationConfigStore: tt.fields.OrganizationConfigStore,
OrganizationConfigStore: tt.fields.organizationConfigStore,
},
Logger: log.New(log.DebugLevel),
}
w := httptest.NewRecorder()
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)
r.Body = ioutil.NopCloser(bytes.NewReader(buf))

View File

@ -486,14 +486,15 @@ func openService(ctx context.Context, buildInfo chronograf.BuildInfo, boltPath s
return Service{
TimeSeriesClient: &InfluxClient{},
Store: &Store{
LayoutsStore: layouts,
DashboardsStore: dashboards,
SourcesStore: sources,
ServersStore: kapacitors,
OrganizationsStore: organizations,
UsersStore: db.UsersStore,
ConfigStore: db.ConfigStore,
MappingsStore: db.MappingsStore,
LayoutsStore: layouts,
DashboardsStore: dashboards,
SourcesStore: sources,
ServersStore: kapacitors,
OrganizationsStore: organizations,
UsersStore: db.UsersStore,
ConfigStore: db.ConfigStore,
MappingsStore: db.MappingsStore,
OrganizationConfigStore: db.OrganizationConfigStore,
},
Logger: logger,
UseAuth: useAuth,

View File

@ -91,6 +91,7 @@ type DataStore interface {
Mappings(ctx context.Context) chronograf.MappingsStore
Dashboards(ctx context.Context) chronograf.DashboardsStore
Config(ctx context.Context) chronograf.ConfigStore
OrganizationConfig(ctx context.Context) chronograf.OrganizationConfigStore
}
// ensure that Store implements a DataStore
@ -98,18 +99,19 @@ var _ DataStore = &Store{}
// Store implements the DataStore interface
type Store struct {
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
MappingsStore chronograf.MappingsStore
OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore
SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
MappingsStore chronograf.MappingsStore
OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore
OrganizationConfigStore chronograf.OrganizationConfigStore
}
// 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 {
if isServer := hasServerContext(ctx); isServer {
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
// and a organization.ServersStore otherwise.
// and an organization.ServersStore otherwise.
func (s *Store) Servers(ctx context.Context) chronograf.ServersStore {
if isServer := hasServerContext(ctx); isServer {
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
// and a organization.DashboardsStore otherwise.
// and an organization.DashboardsStore otherwise.
func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
if isServer := hasServerContext(ctx); isServer {
return s.DashboardsStore
@ -169,17 +171,17 @@ func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
return &noop.DashboardsStore{}
}
// Settings returns a noop.SettingsStore if the context has no organization specified
// and a organization.SettingsStore otherwise.
func (s *Store) Settings(ctx context.Context) chronograf.SettingsStore {
// OrganizationConfig returns a noop.OrganizationConfigStore if the context has no organization specified
// and an organization.OrganizationConfigStore otherwise.
func (s *Store) OrganizationConfig(ctx context.Context) chronograf.OrganizationConfigStore {
if isServer := hasServerContext(ctx); isServer {
return s.SettingsStore
return s.OrganizationConfigStore
}
if org, ok := hasOrganizationContext(ctx); ok {
return organizations.NewSettingsStore(s.SettingsStore, org)
if orgID, ok := hasOrganizationContext(ctx); ok {
return organizations.NewOrganizationConfigStore(s.OrganizationConfigStore, orgID)
}
return &noop.SettingsStore{}
return &noop.OrganizationConfigStore{}
}
// Organizations returns the underlying OrganizationsStore.