From ae77767c9c543da846f5326936b4daf321851924 Mon Sep 17 00:00:00 2001 From: lukevmorris Date: Thu, 30 Mar 2017 09:48:04 -0700 Subject: [PATCH] Allow InfluxDB and Kapacitor configuration via ENV vars or CLI options (#1129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduce Kapacitor and InfluxDB as command line options If omitted, their values will be null at runtime. If supplied, e.g.: chronograf --kapacitor https://path.to.my:1/kapacitor/instance --influxdb https://path.to.my:1/influxdb/instance Their values will be accessible via Server.Kapacitor Server.InfluxDB * MultiSourcesStore will hold Bolt and config’d sources * Delegate to db.SourcesStore for now * Add Username/Password tags for InfluxDB and Kapacitor * Builders for MultiSourceStore and MultiLayoutStore * Store Kapacitor and InfluxDB configs in memory * Typo * Update CHANGELOG * Move StoreBuilders to server/builders.go * Correct these assertions by reversing them * Kapacitor -> KapacitorURL; InfluxDB -> InfluxDBURL --- CHANGELOG.md | 1 + chronograf.go | 2 +- memdb/kapacitors.go | 144 +++++++++++++++++++++++++++++++++++++++ memdb/kapacitors_test.go | 129 +++++++++++++++++++++++++++++++++++ memdb/sources.go | 142 ++++++++++++++++++++++++++++++++++++++ memdb/sources_test.go | 128 ++++++++++++++++++++++++++++++++++ server/builders.go | 113 ++++++++++++++++++++++++++++++ server/server.go | 69 +++++++++++++------ server/server_test.go | 26 +++++++ 9 files changed, 732 insertions(+), 22 deletions(-) create mode 100644 memdb/kapacitors.go create mode 100644 memdb/kapacitors_test.go create mode 100644 memdb/sources.go create mode 100644 memdb/sources_test.go create mode 100644 server/builders.go create mode 100644 server/server_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b87c20d2c..7bd116024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Features 1. [#1112](https://github.com/influxdata/chronograf/pull/1112): Add ability to delete a dashboard 2. [#1120](https://github.com/influxdata/chronograf/pull/1120): Allow users to update user passwords. + 3. [#1129](https://github.com/influxdata/chronograf/pull/1129): Allow InfluxDB and Kapacitor configuration via ENV vars or CLI options ### UI Improvements 1. [#1101](https://github.com/influxdata/chronograf/pull/1101): Compress InfluxQL responses with gzip diff --git a/chronograf.go b/chronograf.go index 021877f1d..83a246703 100644 --- a/chronograf.go +++ b/chronograf.go @@ -137,7 +137,7 @@ type Response interface { // Source is connection information to a time-series data store. type Source struct { - ID int `json:"id,omitempty,string"` // ID is the unique ID of the source + ID int `json:"id,string"` // ID is the unique ID of the source Name string `json:"name"` // Name is the user-defined name for the source Type string `json:"type,omitempty"` // Type specifies which kinds of source (enterprise vs oss) Username string `json:"username,omitempty"` // Username is the username to connect to the source diff --git a/memdb/kapacitors.go b/memdb/kapacitors.go new file mode 100644 index 000000000..83f1fed32 --- /dev/null +++ b/memdb/kapacitors.go @@ -0,0 +1,144 @@ +package memdb + +import ( + "context" + "fmt" + + "github.com/influxdata/chronograf" +) + +// Ensure KapacitorStore and MultiKapacitorStore implements chronograf.ServersStore. +var _ chronograf.ServersStore = &KapacitorStore{} +var _ chronograf.ServersStore = &MultiKapacitorStore{} + +// KapacitorStore implements the chronograf.ServersStore interface, and keeps +// an in-memory Kapacitor according to startup configuration +type KapacitorStore struct { + Kapacitor *chronograf.Server +} + +// All will return a slice containing a configured source +func (store *KapacitorStore) All(ctx context.Context) ([]chronograf.Server, error) { + if store.Kapacitor != nil { + return []chronograf.Server{*store.Kapacitor}, nil + } + return nil, nil +} + +// Add does not have any effect +func (store *KapacitorStore) Add(ctx context.Context, kap chronograf.Server) (chronograf.Server, error) { + return chronograf.Server{}, fmt.Errorf("In-memory KapacitorStore does not support adding a Kapacitor") +} + +// Delete removes the in-memory configured Kapacitor if its ID matches what's provided +func (store *KapacitorStore) Delete(ctx context.Context, kap chronograf.Server) error { + if store.Kapacitor == nil || store.Kapacitor.ID != kap.ID { + return fmt.Errorf("Unable to find Kapacitor with id %d", kap.ID) + } + store.Kapacitor = nil + return nil +} + +// Get returns the in-memory Kapacitor if its ID matches what's provided +func (store *KapacitorStore) Get(ctx context.Context, id int) (chronograf.Server, error) { + if store.Kapacitor == nil || store.Kapacitor.ID != id { + return chronograf.Server{}, fmt.Errorf("Unable to find Kapacitor with id %d", id) + } + return *store.Kapacitor, nil +} + +// Update overwrites the in-memory configured Kapacitor if its ID matches what's provided +func (store *KapacitorStore) Update(ctx context.Context, kap chronograf.Server) error { + if store.Kapacitor == nil || store.Kapacitor.ID != kap.ID { + return fmt.Errorf("Unable to find Kapacitor with id %d", kap.ID) + } + store.Kapacitor = &kap + return nil +} + +// MultiKapacitorStore implements the chronograf.ServersStore interface, and +// delegates to all contained KapacitorStores +type MultiKapacitorStore struct { + Stores []chronograf.ServersStore +} + +// All concatenates the Kapacitors of all contained Stores +func (multi *MultiKapacitorStore) All(ctx context.Context) ([]chronograf.Server, error) { + all := []chronograf.Server{} + kapSet := map[int]struct{}{} + + ok := false + var err error + for _, store := range multi.Stores { + var kaps []chronograf.Server + kaps, err = store.All(ctx) + if err != nil { + // If this Store is unable to return an array of kapacitors, skip to the + // next Store. + continue + } + ok = true // We've received a response from at least one Store + for _, kap := range kaps { + // Enforce that the kapacitor has a unique ID + // If the ID has been seen before, ignore the kapacitor + if _, okay := kapSet[kap.ID]; !okay { // We have a new kapacitor + kapSet[kap.ID] = struct{}{} // We just care that the ID is unique + all = append(all, kap) + } + } + } + if !ok { + return nil, err + } + return all, nil +} + +// Add the kap to the first responsive Store +func (multi *MultiKapacitorStore) Add(ctx context.Context, kap chronograf.Server) (chronograf.Server, error) { + var err error + for _, store := range multi.Stores { + var k chronograf.Server + k, err = store.Add(ctx, kap) + if err == nil { + return k, nil + } + } + return chronograf.Server{}, nil +} + +// Delete delegates to all Stores, returns success if one Store is successful +func (multi *MultiKapacitorStore) Delete(ctx context.Context, kap chronograf.Server) error { + var err error + for _, store := range multi.Stores { + err = store.Delete(ctx, kap) + if err == nil { + return nil + } + } + return err +} + +// Get finds the Source by id among all contained Stores +func (multi *MultiKapacitorStore) Get(ctx context.Context, id int) (chronograf.Server, error) { + var err error + for _, store := range multi.Stores { + var k chronograf.Server + k, err = store.Get(ctx, id) + if err == nil { + return k, nil + } + } + return chronograf.Server{}, nil +} + +// Update the first responsive Store +func (multi *MultiKapacitorStore) Update(ctx context.Context, kap chronograf.Server) error { + var err error + for _, store := range multi.Stores { + err = store.Update(ctx, kap) + if err == nil { + return nil + } + } + return err +} diff --git a/memdb/kapacitors_test.go b/memdb/kapacitors_test.go new file mode 100644 index 000000000..393900d35 --- /dev/null +++ b/memdb/kapacitors_test.go @@ -0,0 +1,129 @@ +package memdb + +import ( + "context" + "testing" + + "github.com/influxdata/chronograf" +) + +func TestInterfaceImplementation(t *testing.T) { + var _ chronograf.ServersStore = &KapacitorStore{} + var _ chronograf.ServersStore = &MultiKapacitorStore{} +} + +func TestKapacitorStoreAll(t *testing.T) { + ctx := context.Background() + + store := KapacitorStore{} + kaps, err := store.All(ctx) + if err != nil { + t.Fatal("All should not throw an error with an empty Store") + } + if len(kaps) != 0 { + t.Fatal("Store should be empty") + } + + store.Kapacitor = &chronograf.Server{} + kaps, err = store.All(ctx) + if err != nil { + t.Fatal("All should not throw an error with an empty Store") + } + if len(kaps) != 1 { + t.Fatal("Store should have 1 element") + } +} + +func TestKapacitorStoreAdd(t *testing.T) { + ctx := context.Background() + + store := KapacitorStore{} + _, err := store.Add(ctx, chronograf.Server{}) + if err == nil { + t.Fatal("Store should not support adding another source") + } +} + +func TestKapacitorStoreDelete(t *testing.T) { + ctx := context.Background() + + store := KapacitorStore{} + err := store.Delete(ctx, chronograf.Server{}) + if err == nil { + t.Fatal("Delete should not operate on an empty Store") + } + + store.Kapacitor = &chronograf.Server{ + ID: 9, + } + err = store.Delete(ctx, chronograf.Server{ + ID: 8, + }) + if err == nil { + t.Fatal("Delete should not remove elements with the wrong ID") + } + + err = store.Delete(ctx, chronograf.Server{ + ID: 9, + }) + if err != nil { + t.Fatal("Delete should remove an element with a matching ID") + } +} + +func TestKapacitorStoreGet(t *testing.T) { + ctx := context.Background() + + store := KapacitorStore{} + _, err := store.Get(ctx, 9) + if err == nil { + t.Fatal("Get should return an error for an empty Store") + } + + store.Kapacitor = &chronograf.Server{ + ID: 9, + } + _, err = store.Get(ctx, 8) + if err == nil { + t.Fatal("Get should return an error if it finds no matches") + } + + store.Kapacitor = &chronograf.Server{ + ID: 9, + } + kap, err := store.Get(ctx, 9) + if err != nil || kap.ID != 9 { + t.Fatal("Get should find the element with a matching ID") + } +} + +func TestKapacitorStoreUpdate(t *testing.T) { + ctx := context.Background() + + store := KapacitorStore{} + err := store.Update(ctx, chronograf.Server{}) + if err == nil { + t.Fatal("Update fhouls return an error for an empty Store") + } + + store.Kapacitor = &chronograf.Server{ + ID: 9, + } + err = store.Update(ctx, chronograf.Server{ + ID: 8, + }) + if err == nil { + t.Fatal("Update should return an error if it finds no matches") + } + + store.Kapacitor = &chronograf.Server{ + ID: 9, + } + err = store.Update(ctx, chronograf.Server{ + ID: 9, + URL: "http://crystal.pepsi.com", + }) + if err != nil || store.Kapacitor.URL != "http://crystal.pepsi.com" { + t.Fatal("Update should overwrite elements with matching IDs") + } +} diff --git a/memdb/sources.go b/memdb/sources.go new file mode 100644 index 000000000..4f1036335 --- /dev/null +++ b/memdb/sources.go @@ -0,0 +1,142 @@ +package memdb + +import ( + "context" + "fmt" + + "github.com/influxdata/chronograf" +) + +// Ensure MultiSourcesStore and SourcesStore implements chronograf.SourcesStore. +var _ chronograf.SourcesStore = &SourcesStore{} +var _ chronograf.SourcesStore = &MultiSourcesStore{} + +// MultiSourcesStore delegates to the SourcesStores that compose it +type MultiSourcesStore struct { + Stores []chronograf.SourcesStore +} + +// All concatenates the Sources of all contained Stores +func (multi *MultiSourcesStore) All(ctx context.Context) ([]chronograf.Source, error) { + all := []chronograf.Source{} + sourceSet := map[int]struct{}{} + + ok := false + var err error + for _, store := range multi.Stores { + var sources []chronograf.Source + sources, err = store.All(ctx) + if err != nil { + // If this Store is unable to return an array of sources, skip to the + // next Store. + continue + } + ok = true // We've received a response from at least one Store + for _, s := range sources { + // Enforce that the source has a unique ID + // If the source has been seen before, don't override what we already have + if _, okay := sourceSet[s.ID]; !okay { // We have a new Source! + sourceSet[s.ID] = struct{}{} // We just care that the ID is unique + all = append(all, s) + } + } + } + if !ok { + return nil, err + } + return all, nil +} + +// Add the src to the first Store to respond successfully +func (multi *MultiSourcesStore) Add(ctx context.Context, src chronograf.Source) (chronograf.Source, error) { + var err error + for _, store := range multi.Stores { + var s chronograf.Source + s, err = store.Add(ctx, src) + if err == nil { + return s, nil + } + } + return chronograf.Source{}, nil +} + +// Delete delegates to all stores, returns success if one Store is successful +func (multi *MultiSourcesStore) Delete(ctx context.Context, src chronograf.Source) error { + var err error + for _, store := range multi.Stores { + err = store.Delete(ctx, src) + if err == nil { + return nil + } + } + return err +} + +// Get finds the Source by id among all contained Stores +func (multi *MultiSourcesStore) Get(ctx context.Context, id int) (chronograf.Source, error) { + var err error + for _, store := range multi.Stores { + var s chronograf.Source + s, err = store.Get(ctx, id) + if err == nil { + return s, nil + } + } + return chronograf.Source{}, err +} + +// Update the first store to return a successful response +func (multi *MultiSourcesStore) Update(ctx context.Context, src chronograf.Source) error { + var err error + for _, store := range multi.Stores { + err = store.Update(ctx, src) + if err == nil { + return nil + } + } + return err +} + +// SourcesStore implements the chronograf.SourcesStore interface +type SourcesStore struct { + Source *chronograf.Source +} + +// Add does not have any effect +func (store *SourcesStore) Add(ctx context.Context, src chronograf.Source) (chronograf.Source, error) { + return chronograf.Source{}, fmt.Errorf("In-memory SourcesStore does not support adding a Source") +} + +// All will return a slice containing a configured source +func (store *SourcesStore) All(ctx context.Context) ([]chronograf.Source, error) { + if store.Source != nil { + return []chronograf.Source{*store.Source}, nil + } + return nil, nil +} + +// Delete removes the SourcesStore.Soruce if it matches the provided Source +func (store *SourcesStore) Delete(ctx context.Context, src chronograf.Source) error { + if store.Source == nil || store.Source.ID != src.ID { + return fmt.Errorf("Unable to find Source with id %d", src.ID) + } + store.Source = nil + return nil +} + +// Get returns the configured source if the id matches +func (store *SourcesStore) Get(ctx context.Context, id int) (chronograf.Source, error) { + if store.Source == nil || store.Source.ID != id { + return chronograf.Source{}, fmt.Errorf("Unable to find Source with id %d", id) + } + return *store.Source, nil +} + +// Update does nothing +func (store *SourcesStore) Update(ctx context.Context, src chronograf.Source) error { + if store.Source == nil || store.Source.ID != src.ID { + return fmt.Errorf("Unable to find Source with id %d", src.ID) + } + store.Source = &src + return nil +} diff --git a/memdb/sources_test.go b/memdb/sources_test.go new file mode 100644 index 000000000..c4fd861e0 --- /dev/null +++ b/memdb/sources_test.go @@ -0,0 +1,128 @@ +package memdb + +import ( + "context" + "testing" + + "github.com/influxdata/chronograf" +) + +func TestSourcesStore(t *testing.T) { + var _ chronograf.SourcesStore = &SourcesStore{} +} + +func TestSourcesStoreAdd(t *testing.T) { + ctx := context.Background() + + store := SourcesStore{} + _, err := store.Add(ctx, chronograf.Source{}) + if err == nil { + t.Fatal("Store should not support adding another source") + } +} + +func TestSourcesStoreAll(t *testing.T) { + ctx := context.Background() + + store := SourcesStore{} + srcs, err := store.All(ctx) + if err != nil { + t.Fatal("All should not throw an error with an empty Store") + } + if len(srcs) != 0 { + t.Fatal("Store should be empty") + } + + store.Source = &chronograf.Source{} + srcs, err = store.All(ctx) + if err != nil { + t.Fatal("All should not throw an error with an empty Store") + } + if len(srcs) != 1 { + t.Fatal("Store should have 1 element") + } +} + +func TestSourcesStoreDelete(t *testing.T) { + ctx := context.Background() + + store := SourcesStore{} + err := store.Delete(ctx, chronograf.Source{}) + if err == nil { + t.Fatal("Delete should not operate on an empty Store") + } + + store.Source = &chronograf.Source{ + ID: 9, + } + err = store.Delete(ctx, chronograf.Source{ + ID: 8, + }) + if err == nil { + t.Fatal("Delete should not remove elements with the wrong ID") + } + + err = store.Delete(ctx, chronograf.Source{ + ID: 9, + }) + if err != nil { + t.Fatal("Delete should remove an element with a matching ID") + } +} + +func TestSourcesStoreGet(t *testing.T) { + ctx := context.Background() + + store := SourcesStore{} + _, err := store.Get(ctx, 9) + if err == nil { + t.Fatal("Get should return an error for an empty Store") + } + + store.Source = &chronograf.Source{ + ID: 9, + } + _, err = store.Get(ctx, 8) + if err == nil { + t.Fatal("Get should return an error if it finds no matches") + } + + store.Source = &chronograf.Source{ + ID: 9, + } + src, err := store.Get(ctx, 9) + if err != nil || src.ID != 9 { + t.Fatal("Get should find the element with a matching ID") + } +} + +func TestSourcesStoreUpdate(t *testing.T) { + ctx := context.Background() + + store := SourcesStore{} + err := store.Update(ctx, chronograf.Source{}) + if err == nil { + t.Fatal("Update should return an error for an empty Store") + } + + store.Source = &chronograf.Source{ + ID: 9, + } + err = store.Update(ctx, chronograf.Source{ + ID: 8, + }) + if err == nil { + t.Fatal("Update should return an error if it finds no matches") + } + + store.Source = &chronograf.Source{ + ID: 9, + } + err = store.Update(ctx, chronograf.Source{ + ID: 9, + URL: "http://crystal.pepsi.com", + }) + if err != nil || store.Source.URL != "http://crystal.pepsi.com" { + t.Fatal("Update should overwrite elements with matching IDs") + } +} diff --git a/server/builders.go b/server/builders.go new file mode 100644 index 000000000..57a43eb84 --- /dev/null +++ b/server/builders.go @@ -0,0 +1,113 @@ +package server + +import ( + "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/canned" + "github.com/influxdata/chronograf/layouts" + "github.com/influxdata/chronograf/memdb" +) + +// LayoutBuilder is responsible for building Layouts +type LayoutBuilder interface { + Build(chronograf.LayoutStore) (*layouts.MultiLayoutStore, error) +} + +// MultiLayoutBuilder implements LayoutBuilder and will return a MultiLayoutStore +type MultiLayoutBuilder struct { + Logger chronograf.Logger + UUID chronograf.ID + CannedPath string +} + +// Build will construct a MultiLayoutStore of canned and db-backed personalized +// layouts +func (builder *MultiLayoutBuilder) Build(db chronograf.LayoutStore) (*layouts.MultiLayoutStore, error) { + // These apps are those handled from a directory + apps := canned.NewApps(builder.CannedPath, builder.UUID, builder.Logger) + // These apps are statically compiled into chronograf + binApps := &canned.BinLayoutStore{ + Logger: builder.Logger, + } + // Acts as a front-end to both the bolt layouts, filesystem layouts and binary statically compiled layouts. + // The idea here is that these stores form a hierarchy in which each is tried sequentially until + // the operation has success. So, the database is preferred over filesystem over binary data. + layouts := &layouts.MultiLayoutStore{ + Stores: []chronograf.LayoutStore{ + db, + apps, + binApps, + }, + } + + return layouts, nil +} + +// SourcesBuilder builds a MultiSourceStore +type SourcesBuilder interface { + Build(chronograf.SourcesStore) (*memdb.MultiSourcesStore, error) +} + +// MultiSourceBuilder implements SourcesBuilder +type MultiSourceBuilder struct { + InfluxDBURL string + InfluxDBUsername string + InfluxDBPassword string +} + +// Build will return a MultiSourceStore +func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*memdb.MultiSourcesStore, error) { + stores := []chronograf.SourcesStore{db} + + if fs.InfluxDBURL != "" { + influxStore := &memdb.SourcesStore{ + Source: &chronograf.Source{ + ID: 0, + Name: fs.InfluxDBURL, + Type: chronograf.InfluxDB, + Username: fs.InfluxDBUsername, + Password: fs.InfluxDBPassword, + URL: fs.InfluxDBURL, + Default: true, + }} + stores = append([]chronograf.SourcesStore{influxStore}, stores...) + } + sources := &memdb.MultiSourcesStore{ + Stores: stores, + } + + return sources, nil +} + +// KapacitorBuilder builds a KapacitorStore +type KapacitorBuilder interface { + Build(chronograf.ServersStore) (*memdb.MultiKapacitorStore, error) +} + +// MultiKapacitorBuilder implements KapacitorBuilder +type MultiKapacitorBuilder struct { + KapacitorURL string + KapacitorUsername string + KapacitorPassword string +} + +// Build will return a MultiKapacitorStore +func (builder *MultiKapacitorBuilder) Build(db chronograf.ServersStore) (*memdb.MultiKapacitorStore, error) { + stores := []chronograf.ServersStore{db} + if builder.KapacitorURL != "" { + memStore := &memdb.KapacitorStore{ + Kapacitor: &chronograf.Server{ + ID: 0, + SrcID: 0, + Name: builder.KapacitorURL, + URL: builder.KapacitorURL, + Username: builder.KapacitorUsername, + Password: builder.KapacitorPassword, + }, + } + stores = append([]chronograf.ServersStore{memStore}, stores...) + } + kapacitors := &memdb.MultiKapacitorStore{ + Stores: stores, + } + return kapacitors, nil +} diff --git a/server/server.go b/server/server.go index a19a0c077..4efb88123 100644 --- a/server/server.go +++ b/server/server.go @@ -14,15 +14,13 @@ import ( "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/bolt" - "github.com/influxdata/chronograf/canned" - "github.com/influxdata/chronograf/layouts" + "github.com/influxdata/chronograf/influx" clog "github.com/influxdata/chronograf/log" "github.com/influxdata/chronograf/oauth2" "github.com/influxdata/chronograf/uuid" client "github.com/influxdata/usage-client/v1" flags "github.com/jessevdk/go-flags" "github.com/tylerb/graceful" - "github.com/influxdata/chronograf/influx" ) var ( @@ -42,6 +40,14 @@ type Server struct { Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"` Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"` + InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` + InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` + InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` + + KapacitorURL string `long:"kapacitor-url" description:"Location of your Kapacitor instance" env:"KAPACITOR_URL"` + KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"` + KapacitorPassword string `long:"kapacitor-password" description:"Password of your Kapacitor instance" env:"KAPACITOR_PASSWORD"` + Develop bool `short:"d" long:"develop" description:"Run server in develop mode."` BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (/var/lib/chronograf/chronograf-v1.db)" env:"BOLT_PATH" default:"chronograf-v1.db"` CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"` @@ -180,7 +186,22 @@ func (s *Server) NewListener() (net.Listener, error) { // Serve starts and runs the chronograf server func (s *Server) Serve(ctx context.Context) error { logger := clog.New(clog.ParseLevel(s.LogLevel)) - service := openService(ctx, s.BoltPath, s.CannedPath, logger, s.useAuth()) + layoutBuilder := &MultiLayoutBuilder{ + Logger: logger, + UUID: &uuid.V4{}, + CannedPath: s.CannedPath, + } + sourcesBuilder := &MultiSourceBuilder{ + InfluxDBURL: s.InfluxDBURL, + InfluxDBUsername: s.InfluxDBUsername, + InfluxDBPassword: s.InfluxDBPassword, + } + kapacitorBuilder := &MultiKapacitorBuilder{ + KapacitorURL: s.KapacitorURL, + KapacitorUsername: s.KapacitorUsername, + KapacitorPassword: s.KapacitorPassword, + } + service := openService(ctx, s.BoltPath, layoutBuilder, sourcesBuilder, kapacitorBuilder, logger, s.useAuth()) basepath = s.Basepath providerFuncs := []func(func(oauth2.Provider, oauth2.Mux)){} @@ -256,7 +277,7 @@ func (s *Server) Serve(ctx context.Context) error { return nil } -func openService(ctx context.Context, boltPath, cannedPath string, logger chronograf.Logger, useAuth bool) Service { +func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, sBuilder SourcesBuilder, kapBuilder KapacitorBuilder, logger chronograf.Logger, useAuth bool) Service { db := bolt.NewClient() db.Path = boltPath if err := db.Open(ctx); err != nil { @@ -266,28 +287,34 @@ func openService(ctx context.Context, boltPath, cannedPath string, logger chrono os.Exit(1) } - // These apps are those handled from a directory - apps := canned.NewApps(cannedPath, &uuid.V4{}, logger) - // These apps are statically compiled into chronograf - binApps := &canned.BinLayoutStore{ - Logger: logger, + layouts, err := lBuilder.Build(db.LayoutStore) + if err != nil { + logger. + WithField("component", "LayoutStore"). + Error("Unable to construct a MultiLayoutStore", err) + os.Exit(1) } - // Acts as a front-end to both the bolt layouts, filesystem layouts and binary statically compiled layouts. - // The idea here is that these stores form a hierarchy in which each is tried sequentially until - // the operation has success. So, the database is preferred over filesystem over binary data. - layouts := &layouts.MultiLayoutStore{ - Stores: []chronograf.LayoutStore{ - db.LayoutStore, - apps, - binApps, - }, + sources, err := sBuilder.Build(db.SourcesStore) + if err != nil { + logger. + WithField("component", "SourcesStore"). + Error("Unable to construct a MultiSourcesStore", err) + os.Exit(1) + } + + kapacitors, err := kapBuilder.Build(db.ServersStore) + if err != nil { + logger. + WithField("component", "KapacitorStore"). + Error("Unable to construct a MultiKapacitorStore", err) + os.Exit(1) } return Service{ TimeSeriesClient: &InfluxClient{}, - SourcesStore: db.SourcesStore, - ServersStore: db.ServersStore, + SourcesStore: sources, + ServersStore: kapacitors, UsersStore: db.UsersStore, LayoutStore: layouts, DashboardsStore: db.DashboardsStore, diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 000000000..829f53faf --- /dev/null +++ b/server/server_test.go @@ -0,0 +1,26 @@ +package server + +import "testing" + +func TestLayoutBuilder(t *testing.T) { + var l LayoutBuilder = &MultiLayoutBuilder{} + layout, err := l.Build(nil) + if err != nil { + t.Fatalf("MultiLayoutBuilder can't build a MultiLayoutStore: %v", err) + } + + if layout == nil { + t.Fatal("LayoutBuilder should have built a layout") + } +} + +func TestSourcesStoresBuilder(t *testing.T) { + var b SourcesBuilder = &MultiSourceBuilder{} + sources, err := b.Build(nil) + if err != nil { + t.Fatalf("MultiSourceBuilder can't build a MultiSourcesStore: %v", err) + } + if sources == nil { + t.Fatal("SourcesBuilder should have built a MultiSourceStore") + } +}