Merge branch 'master' into release-beta4

pull/10616/head
Regan Kuchan 2017-02-24 16:38:24 -08:00
commit abaa2bdff2
76 changed files with 8900 additions and 460 deletions

View File

@ -3,6 +3,7 @@
### Bug Fixes ### Bug Fixes
1. [#882](https://github.com/influxdata/chronograf/pull/882): Fix y-axis graph padding 1. [#882](https://github.com/influxdata/chronograf/pull/882): Fix y-axis graph padding
2. [#907](https://github.com/influxdata/chronograf/pull/907): Fix react-router warning 2. [#907](https://github.com/influxdata/chronograf/pull/907): Fix react-router warning
3. [#926](https://github.com/influxdata/chronograf/pull/926): Fix Kapacitor RuleGraph display
### Features ### Features
1. [#873](https://github.com/influxdata/chronograf/pull/873): Add [TLS](https://github.com/influxdata/chronograf/blob/master/docs/tls.md) support 1. [#873](https://github.com/influxdata/chronograf/pull/873): Add [TLS](https://github.com/influxdata/chronograf/blob/master/docs/tls.md) support
@ -10,13 +11,14 @@
3. [#891](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations draggable 3. [#891](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations draggable
4. [#892](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations resizable 4. [#892](https://github.com/influxdata/chronograf/issues/891): Make dashboard visualizations resizable
5. [#893](https://github.com/influxdata/chronograf/issues/893): Persist dashboard visualization position 5. [#893](https://github.com/influxdata/chronograf/issues/893): Persist dashboard visualization position
6. [#922](https://github.com/influxdata/chronograf/issues/922): Additional OAuth2 support for Heroku and Google 6. [#922](https://github.com/influxdata/chronograf/issues/922): Additional OAuth2 support for [Heroku](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#heroku) and [Google](https://github.com/influxdata/chronograf/blob/master/docs/auth.md#google)
### UI Improvements ### UI Improvements
1. [#905](https://github.com/influxdata/chronograf/pull/905): Make scroll bar thumb element bigger 1. [#905](https://github.com/influxdata/chronograf/pull/905): Make scroll bar thumb element bigger
2. [#917](https://github.com/influxdata/chronograf/pull/917): Simplify side navigation 2. [#917](https://github.com/influxdata/chronograf/pull/917): Simplify side navigation
3. [#920](https://github.com/influxdata/chronograf/pull/920): Display stacked and step plot graphs 3. [#920](https://github.com/influxdata/chronograf/pull/920): Display stacked and step plot graphs
4. [#851](https://github.com/influxdata/chronograf/pull/851): Add configuration for Influx Enterprise Meta nodes 4. [#851](https://github.com/influxdata/chronograf/pull/851): Add configuration for Influx Enterprise Meta nodes
5. [#916](https://github.com/influxdata/chronograf/pull/916): Dynamically scale font size based on resolution
## v1.2.0-beta3 [2017-02-15] ## v1.2.0-beta3 [2017-02-15]

View File

@ -1,4 +1,4 @@
.PHONY: assets dep clean test gotest gotestrace jstest run run-dev ctags .PHONY: assets dep clean test gotest gotestrace jstest run run-dev ctags continuous
VERSION ?= $(shell git describe --always --tags) VERSION ?= $(shell git describe --always --tags)
COMMIT ?= $(shell git rev-parse --short=8 HEAD) COMMIT ?= $(shell git rev-parse --short=8 HEAD)
@ -20,7 +20,7 @@ build: assets ${BINARY}
dev: dep dev-assets ${BINARY} dev: dep dev-assets ${BINARY}
${BINARY}: $(SOURCES) .bindata ${BINARY}: $(SOURCES) .bindata .jsdep .godep
go build -o ${BINARY} ${LDFLAGS} ./cmd/chronograf/main.go go build -o ${BINARY} ${LDFLAGS} ./cmd/chronograf/main.go
docker-${BINARY}: $(SOURCES) docker-${BINARY}: $(SOURCES)
@ -94,7 +94,7 @@ run: ${BINARY}
./chronograf ./chronograf
run-dev: ${BINARY} run-dev: ${BINARY}
./chronograf -d ./chronograf -d --log-level=debug
clean: clean:
if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
@ -103,5 +103,8 @@ clean:
rm -f dist/dist_gen.go canned/bin_gen.go server/swagger_gen.go rm -f dist/dist_gen.go canned/bin_gen.go server/swagger_gen.go
@rm -f .godep .jsdep .jssrc .dev-jssrc .bindata @rm -f .godep .jsdep .jssrc .dev-jssrc .bindata
continuous:
while true; do if fswatch -r --one-event .; then echo "#-> Starting build: `date`"; make dev; pkill chronograf; ./chronograf -d --log-level=debug & echo "#-> Build complete."; fi; sleep 0.5; done
ctags: ctags:
ctags -R --languages="Go" --exclude=.git --exclude=ui . ctags -R --languages="Go" --exclude=.git --exclude=ui .

View File

@ -280,21 +280,35 @@ func UnmarshalAlertRule(data []byte, r *ScopedAlert) error {
} }
// MarshalUser encodes a user to binary protobuf format. // MarshalUser encodes a user to binary protobuf format.
// We are ignoring the password for now.
func MarshalUser(u *chronograf.User) ([]byte, error) { func MarshalUser(u *chronograf.User) ([]byte, error) {
return proto.Marshal(&User{ return MarshalUserPB(&User{
ID: uint64(u.ID), Name: u.Name,
Email: u.Email,
}) })
} }
// MarshalUserPB encodes a user to binary protobuf format.
// We are ignoring the password for now.
func MarshalUserPB(u *User) ([]byte, error) {
return proto.Marshal(u)
}
// UnmarshalUser decodes a user from binary protobuf data. // UnmarshalUser decodes a user from binary protobuf data.
// We are ignoring the password for now.
func UnmarshalUser(data []byte, u *chronograf.User) error { func UnmarshalUser(data []byte, u *chronograf.User) error {
var pb User var pb User
if err := proto.Unmarshal(data, &pb); err != nil { if err := UnmarshalUserPB(data, &pb); err != nil {
return err
}
u.Name = pb.Name
return nil
}
// UnmarshalUser decodes a user from binary protobuf data.
// We are ignoring the password for now.
func UnmarshalUserPB(data []byte, u *User) error {
if err := proto.Unmarshal(data, u); err != nil {
return err return err
} }
u.ID = chronograf.UserID(pb.ID)
u.Email = pb.Email
return nil return nil
} }

View File

@ -199,8 +199,8 @@ func (*AlertRule) ProtoMessage() {}
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} } func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
type User struct { type User struct {
ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
Email string `protobuf:"bytes,2,opt,name=Email,proto3" json:"Email,omitempty"` Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
} }
func (m *User) Reset() { *m = User{} } func (m *User) Reset() { *m = User{} }
@ -224,47 +224,46 @@ func init() {
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{ var fileDescriptorInternal = []byte{
// 662 bytes of a gzipped FileDescriptorProto // 653 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x54, 0xd1, 0x6e, 0xd3, 0x4a, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xbc, 0x54, 0xd1, 0x6e, 0xd3, 0x4a,
0x10, 0xd5, 0xc6, 0x76, 0x12, 0x4f, 0x7b, 0x7b, 0xaf, 0x56, 0xd5, 0xbd, 0xab, 0xfb, 0x14, 0x59, 0x10, 0xd5, 0xc6, 0x76, 0x12, 0x4f, 0x7b, 0x7b, 0xaf, 0x56, 0x57, 0xb0, 0xe2, 0xc9, 0xb2, 0x40,
0x20, 0x05, 0x09, 0xfa, 0x40, 0xbf, 0xa0, 0xad, 0x11, 0x0a, 0xb4, 0xa5, 0x6c, 0x5a, 0x78, 0x02, 0x0a, 0x48, 0xf4, 0x81, 0x7e, 0x41, 0x5b, 0x4b, 0x28, 0xd0, 0x96, 0xb2, 0x69, 0xe1, 0x09, 0xa4,
0x69, 0x9b, 0x4e, 0x1a, 0x0b, 0x27, 0x36, 0x6b, 0x9b, 0xd4, 0xbf, 0x80, 0xf8, 0x02, 0x1e, 0xf8, 0x6d, 0x3a, 0x69, 0x2c, 0x1c, 0xdb, 0xac, 0x6d, 0x52, 0xff, 0x02, 0xe2, 0x0b, 0x78, 0xe0, 0x23,
0x08, 0x7e, 0x85, 0x1f, 0x42, 0xb3, 0xbb, 0x76, 0x5c, 0x51, 0xa1, 0x3e, 0xf1, 0x36, 0x67, 0x66, 0xf8, 0x15, 0x7e, 0x08, 0xcd, 0x7a, 0xed, 0xb8, 0xa2, 0xa0, 0x3e, 0xf1, 0x36, 0x67, 0x66, 0x73,
0x73, 0x66, 0xe6, 0x9c, 0x89, 0x61, 0x27, 0x59, 0x95, 0xa8, 0x57, 0x2a, 0xdd, 0xcb, 0x75, 0x56, 0x66, 0xe6, 0x9c, 0x71, 0x60, 0x27, 0x4e, 0x4b, 0xd4, 0xa9, 0x4a, 0x76, 0x73, 0x9d, 0x95, 0x19,
0x66, 0x7c, 0xd8, 0xe0, 0xe8, 0x73, 0x0f, 0xfa, 0xd3, 0xac, 0xd2, 0x33, 0xe4, 0x3b, 0xd0, 0x9b, 0x1f, 0xb7, 0x38, 0xfc, 0x3c, 0x80, 0xe1, 0x2c, 0xab, 0xf4, 0x1c, 0xf9, 0x0e, 0x0c, 0xa6, 0x91,
0xc4, 0x82, 0x8d, 0xd8, 0xd8, 0x93, 0xbd, 0x49, 0xcc, 0x39, 0xf8, 0xa7, 0x6a, 0x89, 0xa2, 0x37, 0x60, 0x01, 0x9b, 0x38, 0x72, 0x30, 0x8d, 0x38, 0x07, 0xf7, 0x44, 0xad, 0x50, 0x0c, 0x02, 0x36,
0x62, 0xe3, 0x50, 0x9a, 0x98, 0x72, 0xe7, 0x75, 0x8e, 0xc2, 0xb3, 0x39, 0x8a, 0xf9, 0xff, 0x30, 0xf1, 0xa5, 0x89, 0x29, 0x77, 0x56, 0xe7, 0x28, 0x9c, 0x26, 0x47, 0x31, 0x7f, 0x00, 0xe3, 0xf3,
0xbc, 0x28, 0x88, 0x6d, 0x89, 0xc2, 0x37, 0xf9, 0x16, 0x53, 0xed, 0x4c, 0x15, 0xc5, 0x3a, 0xd3, 0x82, 0xd8, 0x56, 0x28, 0x5c, 0x93, 0xef, 0x30, 0xd5, 0x4e, 0x55, 0x51, 0xac, 0x33, 0x7d, 0x29,
0x57, 0x22, 0xb0, 0xb5, 0x06, 0xf3, 0x7f, 0xc0, 0xbb, 0x90, 0xc7, 0xa2, 0x6f, 0xd2, 0x14, 0x72, 0xbc, 0xa6, 0xd6, 0x62, 0xfe, 0x1f, 0x38, 0xe7, 0xf2, 0x48, 0x0c, 0x4d, 0x9a, 0x42, 0x2e, 0x60,
0x01, 0x83, 0x18, 0xe7, 0xaa, 0x4a, 0x4b, 0x31, 0x18, 0xb1, 0xf1, 0x50, 0x36, 0x90, 0x78, 0xce, 0x14, 0xe1, 0x42, 0x55, 0x49, 0x29, 0x46, 0x01, 0x9b, 0x8c, 0x65, 0x0b, 0x89, 0xe7, 0x0c, 0x13,
0x31, 0xc5, 0x6b, 0xad, 0xe6, 0x62, 0x68, 0x79, 0x1a, 0xcc, 0xf7, 0x80, 0x4f, 0x56, 0x05, 0xce, 0xbc, 0xd2, 0x6a, 0x21, 0xc6, 0x0d, 0x4f, 0x8b, 0xf9, 0x2e, 0xf0, 0x69, 0x5a, 0xe0, 0xbc, 0xd2,
0x2a, 0x8d, 0xd3, 0x0f, 0x49, 0xfe, 0x06, 0x75, 0x32, 0xaf, 0x45, 0x68, 0x08, 0xee, 0xa8, 0x50, 0x38, 0xfb, 0x10, 0xe7, 0x6f, 0x50, 0xc7, 0x8b, 0x5a, 0xf8, 0x86, 0xe0, 0x96, 0x0a, 0x75, 0x39,
0x97, 0x13, 0x2c, 0x15, 0xf5, 0x06, 0x43, 0xd5, 0xc0, 0xe8, 0x3d, 0x84, 0xb1, 0x2a, 0x16, 0x97, 0xc6, 0x52, 0x51, 0x6f, 0x30, 0x54, 0x2d, 0x0c, 0xdf, 0x83, 0x1f, 0xa9, 0x62, 0x79, 0x91, 0x29,
0x99, 0xd2, 0x57, 0xf7, 0x92, 0xe3, 0x09, 0x04, 0x33, 0x4c, 0xd3, 0x42, 0x78, 0x23, 0x6f, 0xbc, 0x7d, 0x79, 0x27, 0x39, 0x9e, 0x82, 0x37, 0xc7, 0x24, 0x29, 0x84, 0x13, 0x38, 0x93, 0xad, 0x67,
0xf5, 0xf4, 0xbf, 0xbd, 0x56, 0xe7, 0x96, 0xe7, 0x08, 0xd3, 0x54, 0xda, 0x57, 0xd1, 0x57, 0x06, 0xf7, 0x77, 0x3b, 0x9d, 0x3b, 0x9e, 0x43, 0x4c, 0x12, 0xd9, 0xbc, 0x0a, 0xbf, 0x32, 0xf8, 0xe7,
0x7f, 0xdd, 0x2a, 0xf0, 0x6d, 0x60, 0x37, 0xa6, 0x47, 0x20, 0xd9, 0x0d, 0xa1, 0xda, 0xf0, 0x07, 0x46, 0x81, 0x6f, 0x03, 0xbb, 0x36, 0x3d, 0x3c, 0xc9, 0xae, 0x09, 0xd5, 0x86, 0xdf, 0x93, 0xac,
0x92, 0xd5, 0x84, 0xd6, 0x46, 0xe8, 0x40, 0xb2, 0x35, 0xa1, 0x85, 0x91, 0x37, 0x90, 0x6c, 0xc1, 0x26, 0xb4, 0x36, 0x42, 0x7b, 0x92, 0xad, 0x09, 0x2d, 0x8d, 0xbc, 0x9e, 0x64, 0x4b, 0xfe, 0x18,
0x1f, 0xc1, 0xe0, 0x63, 0x85, 0x3a, 0xc1, 0x42, 0x04, 0xa6, 0xf5, 0xdf, 0x9b, 0xd6, 0xaf, 0x2b, 0x46, 0x1f, 0x2b, 0xd4, 0x31, 0x16, 0xc2, 0x33, 0xad, 0xff, 0xdd, 0xb4, 0x7e, 0x5d, 0xa1, 0xae,
0xd4, 0xb5, 0x6c, 0xea, 0x34, 0xb7, 0xb1, 0xc6, 0xea, 0x6c, 0x62, 0xca, 0x95, 0x64, 0xe3, 0xc0, 0x65, 0x5b, 0xa7, 0xb9, 0x8d, 0x35, 0x8d, 0xce, 0x26, 0xa6, 0x5c, 0x49, 0x36, 0x8e, 0x9a, 0x1c,
0xe6, 0x28, 0x8e, 0xbe, 0x30, 0xe8, 0x4f, 0x51, 0x7f, 0x42, 0x7d, 0xaf, 0xd5, 0xbb, 0xae, 0x7b, 0xc5, 0xe1, 0x17, 0x06, 0xc3, 0x19, 0xea, 0x4f, 0xa8, 0xef, 0xb4, 0x7a, 0xdf, 0x75, 0xe7, 0x0f,
0xbf, 0x71, 0xdd, 0xbf, 0xdb, 0xf5, 0x60, 0xe3, 0xfa, 0x2e, 0x04, 0x53, 0x3d, 0x9b, 0xc4, 0x66, 0xae, 0xbb, 0xb7, 0xbb, 0xee, 0x6d, 0x5c, 0xff, 0x1f, 0xbc, 0x99, 0x9e, 0x4f, 0x23, 0x33, 0xa1,
0x42, 0x4f, 0x5a, 0x10, 0x7d, 0x63, 0xd0, 0x3f, 0x56, 0x75, 0x56, 0x95, 0x9d, 0x71, 0x42, 0x33, 0x23, 0x1b, 0x10, 0x7e, 0x63, 0x30, 0x3c, 0x52, 0x75, 0x56, 0x95, 0xbd, 0x71, 0x7c, 0x33, 0x4e,
0xce, 0x08, 0xb6, 0x0e, 0xf2, 0x3c, 0x4d, 0x66, 0xaa, 0x4c, 0xb2, 0x95, 0x9b, 0xaa, 0x9b, 0xa2, 0x00, 0x5b, 0xfb, 0x79, 0x9e, 0xc4, 0x73, 0x55, 0xc6, 0x59, 0x6a, 0xa7, 0xea, 0xa7, 0xe8, 0xc5,
0x17, 0x27, 0xa8, 0x8a, 0x4a, 0xe3, 0x12, 0x57, 0xa5, 0x9b, 0xaf, 0x9b, 0xe2, 0x0f, 0x20, 0x38, 0x31, 0xaa, 0xa2, 0xd2, 0xb8, 0xc2, 0xb4, 0xb4, 0xf3, 0xf5, 0x53, 0xfc, 0x21, 0x78, 0x87, 0xc6,
0x32, 0xce, 0xf9, 0x46, 0xbe, 0x9d, 0x8d, 0x7c, 0xd6, 0x30, 0x53, 0xa4, 0x45, 0x0e, 0xaa, 0x32, 0x39, 0xd7, 0xc8, 0xb7, 0xb3, 0x91, 0xaf, 0x31, 0xcc, 0x14, 0x69, 0x91, 0xfd, 0xaa, 0xcc, 0x16,
0x9b, 0xa7, 0xd9, 0xda, 0x4c, 0x3c, 0x94, 0x2d, 0x8e, 0x7e, 0x30, 0xf0, 0xff, 0x94, 0x87, 0xdb, 0x49, 0xb6, 0x36, 0x13, 0x8f, 0x65, 0x87, 0xc3, 0x1f, 0x0c, 0xdc, 0xbf, 0xe5, 0xe1, 0x36, 0xb0,
0xc0, 0x12, 0x67, 0x20, 0x4b, 0x5a, 0x47, 0x07, 0x1d, 0x47, 0x05, 0x0c, 0x6a, 0xad, 0x56, 0xd7, 0xd8, 0x1a, 0xc8, 0xe2, 0xce, 0xd1, 0x51, 0xcf, 0x51, 0x01, 0xa3, 0x5a, 0xab, 0xf4, 0x0a, 0x0b,
0x58, 0x88, 0xe1, 0xc8, 0x1b, 0x7b, 0xb2, 0x81, 0xa6, 0x92, 0xaa, 0x4b, 0x4c, 0x0b, 0x11, 0x8e, 0x31, 0x0e, 0x9c, 0x89, 0x23, 0x5b, 0x68, 0x2a, 0x89, 0xba, 0xc0, 0xa4, 0x10, 0x7e, 0xe0, 0xd0,
0x3c, 0x3a, 0x77, 0x07, 0xdb, 0x2b, 0x80, 0xce, 0x15, 0x7c, 0x67, 0x10, 0x98, 0xe6, 0xf4, 0xbb, 0xb9, 0x5b, 0xd8, 0x5d, 0x01, 0xf4, 0xae, 0xe0, 0x3b, 0x03, 0xcf, 0x34, 0xa7, 0xdf, 0x1d, 0x66,
0xa3, 0x6c, 0xb9, 0x54, 0xab, 0x2b, 0x27, 0x7d, 0x03, 0xc9, 0x8f, 0xf8, 0xd0, 0xc9, 0xde, 0x8b, 0xab, 0x95, 0x4a, 0x2f, 0xad, 0xf4, 0x2d, 0x24, 0x3f, 0xa2, 0x03, 0x2b, 0xfb, 0x20, 0x3a, 0x20,
0x0f, 0x09, 0xcb, 0x33, 0x27, 0x72, 0x4f, 0x9e, 0x91, 0x6a, 0xcf, 0x75, 0x56, 0xe5, 0x87, 0xb5, 0x2c, 0x4f, 0xad, 0xc8, 0x03, 0x79, 0x4a, 0xaa, 0x3d, 0xd7, 0x59, 0x95, 0x1f, 0xd4, 0x8d, 0xbc,
0x95, 0x37, 0x94, 0x2d, 0xe6, 0xff, 0x42, 0xff, 0xed, 0x02, 0xb5, 0xdb, 0x39, 0x94, 0x0e, 0xd1, 0xbe, 0xec, 0x30, 0xbf, 0x07, 0xc3, 0xb7, 0x4b, 0xd4, 0x76, 0x67, 0x5f, 0x5a, 0x44, 0x47, 0x70,
0x11, 0x1c, 0xd3, 0x54, 0x6e, 0x4b, 0x0b, 0xf8, 0x43, 0x08, 0x24, 0x6d, 0x61, 0x56, 0xbd, 0x25, 0x44, 0x53, 0xd9, 0x2d, 0x1b, 0xc0, 0x1f, 0x81, 0x27, 0x69, 0x0b, 0xb3, 0xea, 0x0d, 0x81, 0x4c,
0x90, 0x49, 0x4b, 0x5b, 0x8d, 0xf6, 0xdd, 0x33, 0x62, 0xb9, 0xc8, 0x73, 0xd4, 0xee, 0x76, 0x2d, 0x5a, 0x36, 0xd5, 0x70, 0xcf, 0x3e, 0x23, 0x96, 0xf3, 0x3c, 0x47, 0x6d, 0x6f, 0xb7, 0x01, 0x86,
0x30, 0xdc, 0xd9, 0x1a, 0xb5, 0x19, 0xd9, 0x93, 0x16, 0x44, 0xef, 0x20, 0x3c, 0x48, 0x51, 0x97, 0x3b, 0x5b, 0xa3, 0x36, 0x23, 0x3b, 0xb2, 0x01, 0xe1, 0x3b, 0xf0, 0xf7, 0x13, 0xd4, 0xa5, 0xac,
0xb2, 0x4a, 0xf1, 0x97, 0x13, 0xe3, 0xe0, 0xbf, 0x98, 0xbe, 0x3a, 0x6d, 0x2e, 0x9e, 0xe2, 0xcd, 0x12, 0xfc, 0xe5, 0xc4, 0x38, 0xb8, 0x2f, 0x66, 0xaf, 0x4e, 0xda, 0x8b, 0xa7, 0x78, 0x73, 0xa7,
0x9d, 0x7a, 0x9d, 0x3b, 0xa5, 0x85, 0x5e, 0xaa, 0x5c, 0x4d, 0x62, 0x63, 0xac, 0x27, 0x1d, 0x8a, 0x4e, 0xef, 0x4e, 0x69, 0xa1, 0x97, 0x2a, 0x57, 0xd3, 0xc8, 0x18, 0xeb, 0x48, 0x8b, 0xc2, 0x27,
0x1e, 0x83, 0x4f, 0xff, 0x87, 0x0e, 0xb3, 0x6f, 0x98, 0x77, 0x21, 0x78, 0xb6, 0x54, 0x49, 0xea, 0xe0, 0xd2, 0xf7, 0xd0, 0x63, 0x76, 0x7f, 0xf7, 0x2d, 0x5d, 0x0c, 0xcd, 0xbf, 0xf2, 0xde, 0xcf,
0xa8, 0x2d, 0xb8, 0xec, 0x9b, 0xef, 0xf2, 0xfe, 0xcf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbd, 0x59, 0x00, 0x00, 0x00, 0xff, 0xff, 0xfa, 0x57, 0xfe, 0xff, 0xa7, 0x05, 0x00, 0x00,
0x67, 0x12, 0xa9, 0x05, 0x00, 0x00,
} }

View File

@ -83,6 +83,6 @@ message AlertRule {
} }
message User { message User {
uint64 ID = 1; // ID is the unique ID of this user uint64 ID = 1; // ID is the unique ID of this user
string Email = 2; // Email byte representation of the user string Name = 2; // Name is the user's login name
} }

View File

@ -202,23 +202,23 @@ func (s *SourcesStore) setRandomDefault(ctx context.Context, src chronograf.Sour
return err return err
} else if target.Default { } else if target.Default {
// Locate another source to be the new default // Locate another source to be the new default
if srcs, err := s.all(ctx, tx); err != nil { srcs, err := s.all(ctx, tx)
if err != nil {
return err return err
} else { }
var other *chronograf.Source var other *chronograf.Source
for idx, _ := range srcs { for idx := range srcs {
other = &srcs[idx] other = &srcs[idx]
// avoid selecting the source we're about to delete as the new default // avoid selecting the source we're about to delete as the new default
if other.ID != target.ID { if other.ID != target.ID {
break break
}
} }
}
// set the other to be the default // set the other to be the default
other.Default = true other.Default = true
if err := s.update(ctx, *other, tx); err != nil { if err := s.update(ctx, *other, tx); err != nil {
return err return err
}
} }
} }
return nil return nil

View File

@ -11,31 +11,36 @@ import (
// Ensure UsersStore implements chronograf.UsersStore. // Ensure UsersStore implements chronograf.UsersStore.
var _ chronograf.UsersStore = &UsersStore{} var _ chronograf.UsersStore = &UsersStore{}
var UsersBucket = []byte("Users") // UsersBucket is used to store users local to chronograf
var UsersBucket = []byte("UsersV1")
// UsersStore uses bolt to store and retrieve users
type UsersStore struct { type UsersStore struct {
client *Client client *Client
} }
// FindByEmail searches the UsersStore for all users owned with the email // get searches the UsersStore for user with name and returns the bolt representation
func (s *UsersStore) FindByEmail(ctx context.Context, email string) (*chronograf.User, error) { func (s *UsersStore) get(ctx context.Context, name string) (*internal.User, error) {
var user chronograf.User found := false
var user internal.User
err := s.client.db.View(func(tx *bolt.Tx) error { err := s.client.db.View(func(tx *bolt.Tx) error {
err := tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error { err := tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error {
var u chronograf.User var u chronograf.User
if err := internal.UnmarshalUser(v, &u); err != nil { if err := internal.UnmarshalUser(v, &u); err != nil {
return err return err
} else if u.Email != email { } else if u.Name != name {
return nil return nil
} }
user.Email = u.Email found = true
user.ID = u.ID if err := internal.UnmarshalUserPB(v, &user); err != nil {
return err
}
return nil return nil
}) })
if err != nil { if err != nil {
return err return err
} }
if user.ID == 0 { if found == false {
return chronograf.ErrUserNotFound return chronograf.ErrUserNotFound
} }
return nil return nil
@ -47,7 +52,18 @@ func (s *UsersStore) FindByEmail(ctx context.Context, email string) (*chronograf
return &user, nil return &user, nil
} }
// Create a new Users in the UsersStore. // Get searches the UsersStore for user with name
func (s *UsersStore) Get(ctx context.Context, name string) (*chronograf.User, error) {
u, err := s.get(ctx, name)
if err != nil {
return nil, err
}
return &chronograf.User{
Name: u.Name,
}, nil
}
// Add a new Users in the UsersStore.
func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
if err := s.client.db.Update(func(tx *bolt.Tx) error { if err := s.client.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(UsersBucket) b := tx.Bucket(UsersBucket)
@ -55,11 +71,9 @@ func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.U
if err != nil { if err != nil {
return err return err
} }
u.ID = chronograf.UserID(seq)
if v, err := internal.MarshalUser(u); err != nil { if v, err := internal.MarshalUser(u); err != nil {
return err return err
} else if err := b.Put(itob(int(u.ID)), v); err != nil { } else if err := b.Put(u64tob(seq), v); err != nil {
return err return err
} }
return nil return nil
@ -71,9 +85,13 @@ func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.U
} }
// Delete the users from the UsersStore // Delete the users from the UsersStore
func (s *UsersStore) Delete(ctx context.Context, u *chronograf.User) error { func (s *UsersStore) Delete(ctx context.Context, user *chronograf.User) error {
u, err := s.get(ctx, user.Name)
if err != nil {
return err
}
if err := s.client.db.Update(func(tx *bolt.Tx) error { if err := s.client.db.Update(func(tx *bolt.Tx) error {
if err := tx.Bucket(UsersBucket).Delete(itob(int(u.ID))); err != nil { if err := tx.Bucket(UsersBucket).Delete(u64tob(u.ID)); err != nil {
return err return err
} }
return nil return nil
@ -84,13 +102,39 @@ func (s *UsersStore) Delete(ctx context.Context, u *chronograf.User) error {
return nil return nil
} }
// Get retrieves a user by id. // Update a user
func (s *UsersStore) Get(ctx context.Context, id chronograf.UserID) (*chronograf.User, error) { func (s *UsersStore) Update(ctx context.Context, usr *chronograf.User) error {
var u chronograf.User u, err := s.get(ctx, usr.Name)
if err != nil {
return err
}
if err := s.client.db.Update(func(tx *bolt.Tx) error {
u.Name = usr.Name
if v, err := internal.MarshalUserPB(u); err != nil {
return err
} else if err := tx.Bucket(UsersBucket).Put(u64tob(u.ID), v); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}
// All returns all users
func (s *UsersStore) All(ctx context.Context) ([]chronograf.User, error) {
var users []chronograf.User
if err := s.client.db.View(func(tx *bolt.Tx) error { if err := s.client.db.View(func(tx *bolt.Tx) error {
if v := tx.Bucket(UsersBucket).Get(itob(int(id))); v == nil { if err := tx.Bucket(UsersBucket).ForEach(func(k, v []byte) error {
return chronograf.ErrUserNotFound var user chronograf.User
} else if err := internal.UnmarshalUser(v, &u); err != nil { if err := internal.UnmarshalUser(v, &user); err != nil {
return err
}
users = append(users, user)
return nil
}); err != nil {
return err return err
} }
return nil return nil
@ -98,32 +142,5 @@ func (s *UsersStore) Get(ctx context.Context, id chronograf.UserID) (*chronograf
return nil, err return nil, err
} }
return &u, nil return users, nil
}
// Update a user
func (s *UsersStore) Update(ctx context.Context, usr *chronograf.User) error {
if err := s.client.db.Update(func(tx *bolt.Tx) error {
// Retrieve an existing user with the same ID.
var u chronograf.User
b := tx.Bucket(UsersBucket)
if v := b.Get(itob(int(usr.ID))); v == nil {
return chronograf.ErrUserNotFound
} else if err := internal.UnmarshalUser(v, &u); err != nil {
return err
}
u.Email = usr.Email
if v, err := internal.MarshalUser(&u); err != nil {
return err
} else if err := b.Put(itob(int(u.ID)), v); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
} }

257
bolt/users_test.go Normal file
View File

@ -0,0 +1,257 @@
package bolt_test
import (
"context"
"reflect"
"testing"
"github.com/influxdata/chronograf"
)
func TestUsersStore_Get(t *testing.T) {
type args struct {
ctx context.Context
name string
}
tests := []struct {
name string
args args
want *chronograf.User
wantErr bool
}{
{
name: "User not found",
args: args{
ctx: context.Background(),
name: "unknown",
},
wantErr: true,
},
}
for _, tt := range tests {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
if err := client.Open(); err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.UsersStore
got, err := s.Get(tt.args.ctx, tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. UsersStore.Get() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestUsersStore_Add(t *testing.T) {
type args struct {
ctx context.Context
u *chronograf.User
}
tests := []struct {
name string
args args
want *chronograf.User
wantErr bool
}{
{
name: "Add new user",
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
},
},
want: &chronograf.User{
Name: "docbrown",
},
},
}
for _, tt := range tests {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
if err := client.Open(); err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.UsersStore
got, err := s.Add(tt.args.ctx, tt.args.u)
if (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. UsersStore.Add() = %v, want %v", tt.name, got, tt.want)
}
got, _ = s.Get(tt.args.ctx, got.Name)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. UsersStore.Add() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestUsersStore_Delete(t *testing.T) {
type args struct {
ctx context.Context
user *chronograf.User
}
tests := []struct {
name string
args args
addFirst bool
wantErr bool
}{
{
name: "No such user",
args: args{
ctx: context.Background(),
user: &chronograf.User{
Name: "noone",
},
},
wantErr: true,
},
{
name: "Delete new user",
args: args{
ctx: context.Background(),
user: &chronograf.User{
Name: "noone",
},
},
addFirst: true,
},
}
for _, tt := range tests {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
if err := client.Open(); err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.UsersStore
if tt.addFirst {
s.Add(tt.args.ctx, tt.args.user)
}
if err := s.Delete(tt.args.ctx, tt.args.user); (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
}
}
func TestUsersStore_Update(t *testing.T) {
type args struct {
ctx context.Context
usr *chronograf.User
}
tests := []struct {
name string
args args
addFirst bool
wantErr bool
}{
{
name: "No such user",
args: args{
ctx: context.Background(),
usr: &chronograf.User{
Name: "noone",
},
},
wantErr: true,
},
{
name: "Update new user",
args: args{
ctx: context.Background(),
usr: &chronograf.User{
Name: "noone",
},
},
addFirst: true,
},
}
for _, tt := range tests {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
if err := client.Open(); err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.UsersStore
if tt.addFirst {
s.Add(tt.args.ctx, tt.args.usr)
}
if err := s.Update(tt.args.ctx, tt.args.usr); (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
}
}
func TestUsersStore_All(t *testing.T) {
tests := []struct {
name string
ctx context.Context
want []chronograf.User
addFirst bool
wantErr bool
}{
{
name: "No users",
},
{
name: "Update new user",
want: []chronograf.User{
{
Name: "howdy",
},
{
Name: "doody",
},
},
addFirst: true,
},
}
for _, tt := range tests {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
if err := client.Open(); err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.UsersStore
if tt.addFirst {
for _, u := range tt.want {
s.Add(tt.ctx, &u)
}
}
got, err := s.All(tt.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. UsersStore.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. UsersStore.All() = %v, want %v", tt.name, got, tt.want)
}
}
}

View File

@ -10,3 +10,10 @@ func itob(v int) []byte {
binary.BigEndian.PutUint64(b, uint64(v)) binary.BigEndian.PutUint64(b, uint64(v))
return b return b
} }
// u64tob returns an 8-byte big endian representation of v.
func u64tob(v uint64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, v)
return b
}

View File

@ -14,6 +14,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"BytesPerSec\")) AS \"bytes_per_sec\" FROM apache", "query": "SELECT non_negative_derivative(max(\"BytesPerSec\")) AS \"bytes_per_sec\" FROM apache",
"label": "bytes/s",
"groupbys": [ "groupbys": [
"\"server\"" "\"server\""
], ],
@ -31,6 +32,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"ReqPerSec\")) AS \"req_per_sec\" FROM apache", "query": "SELECT non_negative_derivative(max(\"ReqPerSec\")) AS \"req_per_sec\" FROM apache",
"label": "requests/s",
"groupbys": [ "groupbys": [
"\"server\"" "\"server\""
], ],
@ -48,6 +50,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"TotalAccesses\")) AS \"tot_access\" FROM apache", "query": "SELECT non_negative_derivative(max(\"TotalAccesses\")) AS \"tot_access\" FROM apache",
"label": "accesses/s",
"groupbys": [ "groupbys": [
"\"server\"" "\"server\""
], ],

View File

@ -14,6 +14,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT count(\"check_id\") as \"Number Critical\" FROM consul_health_checks", "query": "SELECT count(\"check_id\") as \"Number Critical\" FROM consul_health_checks",
"label": "count",
"groupbys": [ "groupbys": [
"\"service_name\"" "\"service_name\""
], ],
@ -33,6 +34,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT count(\"check_id\") as \"Number Warning\" FROM consul_health_checks", "query": "SELECT count(\"check_id\") as \"Number Warning\" FROM consul_health_checks",
"label": "count",
"groupbys": [ "groupbys": [
"\"service_name\"" "\"service_name\""
], ],

View File

@ -14,6 +14,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT mean(\"usage_user\") AS \"usage_user\" FROM \"cpu\"", "query": "SELECT mean(\"usage_user\") AS \"usage_user\" FROM \"cpu\"",
"label": "% CPU time",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }

View File

@ -14,6 +14,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT mean(\"used_percent\") AS \"used_percent\" FROM disk", "query": "SELECT mean(\"used_percent\") AS \"used_percent\" FROM disk",
"label": "% used",
"groupbys": [ "groupbys": [
"\"path\"" "\"path\""
], ],

View File

@ -10,10 +10,11 @@
"w": 4, "w": 4,
"h": 4, "h": 4,
"i": "4c79cefb-5152-410c-9b88-74f9bff7ef22", "i": "4c79cefb-5152-410c-9b88-74f9bff7ef22",
"name": "Docker - Container CPU", "name": "Docker - Container CPU %",
"queries": [ "queries": [
{ {
"query": "SELECT mean(\"usage_percent\") AS \"usage_percent\" FROM \"docker_container_cpu\"", "query": "SELECT mean(\"usage_percent\") AS \"usage_percent\" FROM \"docker_container_cpu\"",
"label": "% CPU time",
"groupbys": [ "groupbys": [
"\"container_name\"" "\"container_name\""
] ]
@ -27,10 +28,11 @@
"w": 4, "w": 4,
"h": 4, "h": 4,
"i": "4c79cefb-5152-410c-9b88-74f9bff7ef00", "i": "4c79cefb-5152-410c-9b88-74f9bff7ef00",
"name": "Docker - Container Memory", "name": "Docker - Container Memory (MB)",
"queries": [ "queries": [
{ {
"query": "SELECT mean(\"usage\") AS \"usage\" FROM \"docker_container_mem\"", "query": "SELECT mean(\"usage\") / 1048576 AS \"usage\" FROM \"docker_container_mem\"",
"label": "MB",
"groupbys": [ "groupbys": [
"\"container_name\"" "\"container_name\""
] ]
@ -48,6 +50,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT max(\"n_containers\") AS \"max_n_containers\" FROM \"docker\"", "query": "SELECT max(\"n_containers\") AS \"max_n_containers\" FROM \"docker\"",
"label": "count",
"groupbys": [ "groupbys": [
"\"host\"" "\"host\""
] ]
@ -82,6 +85,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT max(\"n_containers_running\") AS \"max_n_containers_running\" FROM \"docker\"", "query": "SELECT max(\"n_containers_running\") AS \"max_n_containers_running\" FROM \"docker\"",
"label": "count",
"groupbys": [ "groupbys": [
"\"host\"" "\"host\""
] ]

View File

@ -13,7 +13,8 @@
"name": "InfluxDB - Write HTTP Requests", "name": "InfluxDB - Write HTTP Requests",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"writeReq\"), 1s) AS \"http_requests\" FROM \"influxdb_httpd\"", "query": "SELECT non_negative_derivative(max(\"writeReq\")) AS \"http_requests\" FROM \"influxdb_httpd\"",
"label": "count/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -28,13 +29,15 @@
"name": "InfluxDB - Query Requests", "name": "InfluxDB - Query Requests",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"queryReq\"), 1s) AS \"query_requests\" FROM \"influxdb_httpd\"", "query": "SELECT non_negative_derivative(max(\"queryReq\")) AS \"query_requests\" FROM \"influxdb_httpd\"",
"label": "count/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
] ]
}, },
{ {
"type": "line-stepplot",
"x": 0, "x": 0,
"y": 0, "y": 0,
"w": 4, "w": 4,
@ -43,7 +46,8 @@
"name": "InfluxDB - Client Failures", "name": "InfluxDB - Client Failures",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"clientError\"), 1s) AS \"client_errors\" FROM \"influxdb_httpd\"", "query": "SELECT non_negative_derivative(max(\"clientError\")) AS \"client_errors\" FROM \"influxdb_httpd\"",
"label": "count/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
}, },

View File

@ -13,7 +13,8 @@
"name": "InfluxDB - Write Points", "name": "InfluxDB - Write Points",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"pointReq\"), 1s) AS \"points_written\" FROM \"influxdb_write\"", "query": "SELECT non_negative_derivative(max(\"pointReq\")) AS \"points_written\" FROM \"influxdb_write\"",
"label": "points/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -28,12 +29,13 @@
"name": "InfluxDB - Write Errors", "name": "InfluxDB - Write Errors",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"writeError\"), 1s) AS \"shard_write_error\" FROM \"influxdb_write\"", "query": "SELECT non_negative_derivative(max(\"writeError\")) AS \"shard_write_error\" FROM \"influxdb_write\"",
"label": "errors/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
}, },
{ {
"query": "SELECT non_negative_derivative(max(\"serveError\"), 1s) AS \"http_error\" FROM \"influxdb_httpd\"", "query": "SELECT non_negative_derivative(max(\"serveError\")) AS \"http_error\" FROM \"influxdb_httpd\"",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }

View File

@ -10,10 +10,11 @@
"w": 4, "w": 4,
"h": 4, "h": 4,
"i": "e6e5063c-43d5-409b-a0ab-68da51ed3f28", "i": "e6e5063c-43d5-409b-a0ab-68da51ed3f28",
"name": "System - Memory Bytes Used", "name": "System - Memory Gigabytes Used",
"queries": [ "queries": [
{ {
"query": "SELECT mean(\"used\") AS \"used\", mean(\"available\") AS \"available\" FROM \"mem\"", "query": "SELECT mean(\"used\") / 1073741824 AS \"used\", mean(\"available\") / 1073741824 AS \"available\" FROM \"mem\"",
"label": "GB",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }

View File

@ -14,6 +14,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT max(\"curr_connections\") AS \"current_connections\" FROM memcached", "query": "SELECT max(\"curr_connections\") AS \"current_connections\" FROM memcached",
"label": "count",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -28,7 +29,8 @@
"name": "Memcached - Get Hits/Second", "name": "Memcached - Get Hits/Second",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"get_hits\"), 1s) AS \"get_hits\" FROM memcached", "query": "SELECT non_negative_derivative(max(\"get_hits\")) AS \"get_hits\" FROM memcached",
"label": "hits/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -43,7 +45,8 @@
"name": "Memcached - Get Misses/Second", "name": "Memcached - Get Misses/Second",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"get_misses\"), 1s) AS \"get_misses\" FROM memcached", "query": "SELECT non_negative_derivative(max(\"get_misses\")) AS \"get_misses\" FROM memcached",
"label": "misses/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -58,7 +61,8 @@
"name": "Memcached - Delete Hits/Second", "name": "Memcached - Delete Hits/Second",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"delete_hits\"), 1s) AS \"delete_hits\" FROM memcached", "query": "SELECT non_negative_derivative(max(\"delete_hits\")) AS \"delete_hits\" FROM memcached",
"label": "deletes/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -73,7 +77,8 @@
"name": "Memcached - Delete Misses/Second", "name": "Memcached - Delete Misses/Second",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"delete_misses\"), 1s) AS \"delete_misses\" FROM memcached", "query": "SELECT non_negative_derivative(max(\"delete_misses\")) AS \"delete_misses\" FROM memcached",
"label": "delete misses/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -88,7 +93,8 @@
"name": "Memcached - Incr Hits/Second", "name": "Memcached - Incr Hits/Second",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"incr_hits\"), 1s) AS \"incr_hits\" FROM memcached", "query": "SELECT non_negative_derivative(max(\"incr_hits\")) AS \"incr_hits\" FROM memcached",
"label": "incr hits/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -103,7 +109,8 @@
"name": "Memcached - Incr Misses/Second", "name": "Memcached - Incr Misses/Second",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"incr_misses\"), 1s) AS \"incr_misses\" FROM memcached", "query": "SELECT non_negative_derivative(max(\"incr_misses\")) AS \"incr_misses\" FROM memcached",
"label": "incr misses/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -119,6 +126,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT max(\"curr_items\") AS \"current_items\" FROM memcached", "query": "SELECT max(\"curr_items\") AS \"current_items\" FROM memcached",
"label": "count",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -134,6 +142,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT max(\"total_items\") AS \"total_items\" FROM memcached", "query": "SELECT max(\"total_items\") AS \"total_items\" FROM memcached",
"label": "count",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -149,6 +158,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT max(\"bytes\") AS \"bytes\" FROM memcached", "query": "SELECT max(\"bytes\") AS \"bytes\" FROM memcached",
"label": "bytes",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -163,7 +173,8 @@
"name": "Memcached - Bytes Read/Sec", "name": "Memcached - Bytes Read/Sec",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"bytes_read\"), 1s) AS \"bytes_read\" FROM memcached", "query": "SELECT non_negative_derivative(max(\"bytes_read\")) AS \"bytes_read\" FROM memcached",
"label": "bytes/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -178,7 +189,8 @@
"name": "Memcached - Bytes Written/Sec", "name": "Memcached - Bytes Written/Sec",
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"bytes_written\"), 1s) AS \"bytes_written\" FROM memcached", "query": "SELECT non_negative_derivative(max(\"bytes_written\")) AS \"bytes_written\" FROM memcached",
"label": "bytes/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -194,6 +206,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT non_negative_derivative(max(\"evictions\"), 10s) AS \"evictions\" FROM memcached", "query": "SELECT non_negative_derivative(max(\"evictions\"), 10s) AS \"evictions\" FROM memcached",
"label": "evictions / 10s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }

View File

@ -14,6 +14,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT mean(queries_per_sec) AS queries_per_second, mean(getmores_per_sec) AS getmores_per_second FROM mongodb", "query": "SELECT mean(queries_per_sec) AS queries_per_second, mean(getmores_per_sec) AS getmores_per_second FROM mongodb",
"label": "reads/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -29,6 +30,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT mean(inserts_per_sec) AS inserts_per_second, mean(updates_per_sec) AS updates_per_second, mean(deletes_per_sec) AS deletes_per_second FROM mongodb", "query": "SELECT mean(inserts_per_sec) AS inserts_per_second, mean(updates_per_sec) AS updates_per_second, mean(deletes_per_sec) AS deletes_per_second FROM mongodb",
"label": "writes/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -44,6 +46,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT mean(open_connections) AS open_connections FROM mongodb", "query": "SELECT mean(open_connections) AS open_connections FROM mongodb",
"label": "count",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -59,6 +62,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT max(queued_reads) AS queued_reads, max(queued_writes) as queued_writes FROM mongodb", "query": "SELECT max(queued_reads) AS queued_reads, max(queued_writes) as queued_writes FROM mongodb",
"label": "count",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -74,6 +78,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT mean(net_in_bytes) AS net_in_bytes, mean(net_out_bytes) as net_out_bytes FROM mongodb", "query": "SELECT mean(net_in_bytes) AS net_in_bytes, mean(net_out_bytes) as net_out_bytes FROM mongodb",
"label": "bytes/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -89,6 +94,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT mean(page_faults_per_sec) AS page_faults_per_second FROM mongodb", "query": "SELECT mean(page_faults_per_sec) AS page_faults_per_second FROM mongodb",
"label": "faults/s",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }
@ -104,6 +110,7 @@
"queries": [ "queries": [
{ {
"query": "SELECT mean(vsize_megabytes) AS virtual_memory_megabytes, mean(resident_megabytes) as resident_memory_megabytes FROM mongodb", "query": "SELECT mean(vsize_megabytes) AS virtual_memory_megabytes, mean(resident_megabytes) as resident_memory_megabytes FROM mongodb",
"label": "MB",
"groupbys": [], "groupbys": [],
"wheres": [] "wheres": []
} }

View File

@ -15,6 +15,8 @@ const (
ErrUserNotFound = Error("user not found") ErrUserNotFound = Error("user not found")
ErrLayoutInvalid = Error("layout is invalid") ErrLayoutInvalid = Error("layout is invalid")
ErrAlertNotFound = Error("alert not found") ErrAlertNotFound = Error("alert not found")
ErrAuthentication = Error("user not authenticated")
ErrUninitialized = Error("client uninitialized. Call Open() method")
) )
// Error is a domain error encountered while processing chronograf requests // Error is a domain error encountered while processing chronograf requests
@ -49,6 +51,33 @@ type TimeSeries interface {
Query(context.Context, Query) (Response, error) Query(context.Context, Query) (Response, error)
// Connect will connect to the time series using the information in `Source`. // Connect will connect to the time series using the information in `Source`.
Connect(context.Context, *Source) error Connect(context.Context, *Source) error
// UsersStore represents the user accounts within the TimeSeries database
Users(context.Context) UsersStore
// Allowances returns all valid names permissions in this database
Allowances(context.Context) Allowances
// Roles represents the roles associated with this TimesSeriesDatabase
Roles(context.Context) (RolesStore, error)
}
// Role is a restricted set of permissions assigned to a set of users.
type Role struct {
Name string `json:"name"`
Permissions Permissions `json:"permissions,omitempty"`
Users []User `json:"users,omitempty"`
}
// RolesStore is the Storage and retrieval of authentication information
type RolesStore interface {
// All lists all roles from the RolesStore
All(context.Context) ([]Role, error)
// Create a new Role in the RolesStore
Add(context.Context, *Role) (*Role, error)
// Delete the Role from the RolesStore
Delete(context.Context, *Role) error
// Get retrieves a role if name exists.
Get(ctx context.Context, name string) (*Role, error)
// Update the roles' users or permissions
Update(context.Context, *Role) error
} }
// Range represents an upper and lower bound for data // Range represents an upper and lower bound for data
@ -217,27 +246,49 @@ type ID interface {
Generate() (string, error) Generate() (string, error)
} }
// UserID is a unique ID for a source user. const (
type UserID int // AllScope grants permission for all databases.
AllScope Scope = "all"
// DBScope grants permissions for a specific database
DBScope Scope = "database"
)
// Permission is a specific allowance for User or Role bound to a
// scope of the data source
type Permission struct {
Scope Scope `json:"scope"`
Name string `json:"name,omitempty"`
Allowed Allowances `json:"allowed"`
}
// Permissions represent the entire set of permissions a User or Role may have
type Permissions []Permission
// Allowances defines what actions a user can have on a scoped permission
type Allowances []string
// Scope defines the location of access of a permission
type Scope string
// User represents an authenticated user. // User represents an authenticated user.
type User struct { type User struct {
ID UserID `json:"id"` Name string `json:"name"`
Email string `json:"email"` Passwd string `json:"password"`
Permissions Permissions `json:"permissions,omitempty"`
} }
// UsersStore is the Storage and retrieval of authentication information // UsersStore is the Storage and retrieval of authentication information
type UsersStore interface { type UsersStore interface {
// All lists all users from the UsersStore
All(context.Context) ([]User, error)
// Create a new User in the UsersStore // Create a new User in the UsersStore
Add(context.Context, *User) (*User, error) Add(context.Context, *User) (*User, error)
// Delete the User from the UsersStore // Delete the User from the UsersStore
Delete(context.Context, *User) error Delete(context.Context, *User) error
// Get retrieves a user if `ID` exists. // Get retrieves a user if name exists.
Get(ctx context.Context, ID UserID) (*User, error) Get(ctx context.Context, name string) (*User, error)
// Update the user's permissions or roles // Update the user's permissions or roles
Update(context.Context, *User) error Update(context.Context, *User) error
// FindByEmail will retrieve a user by email address.
FindByEmail(ctx context.Context, Email string) (*User, error)
} }
// DashboardID is the dashboard ID // DashboardID is the dashboard ID

198
enterprise/enterprise.go Normal file
View File

@ -0,0 +1,198 @@
package enterprise
import (
"container/ring"
"net/url"
"strings"
"context"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx"
)
var _ chronograf.TimeSeries = &Client{}
// Ctrl represents administrative controls over an Influx Enterprise cluster
type Ctrl interface {
ShowCluster(ctx context.Context) (*Cluster, error)
Users(ctx context.Context, name *string) (*Users, error)
User(ctx context.Context, name string) (*User, error)
CreateUser(ctx context.Context, name, passwd string) error
DeleteUser(ctx context.Context, name string) error
ChangePassword(ctx context.Context, name, passwd string) error
SetUserPerms(ctx context.Context, name string, perms Permissions) error
Roles(ctx context.Context, name *string) (*Roles, error)
Role(ctx context.Context, name string) (*Role, error)
CreateRole(ctx context.Context, name string) error
DeleteRole(ctx context.Context, name string) error
SetRolePerms(ctx context.Context, name string, perms Permissions) error
SetRoleUsers(ctx context.Context, name string, users []string) error
}
// Client is a device for retrieving time series data from an Influx Enterprise
// cluster. It is configured using the addresses of one or more meta node URLs.
// Data node URLs are retrieved automatically from the meta nodes and queries
// are appropriately load balanced across the cluster.
type Client struct {
Ctrl
UsersStore chronograf.UsersStore
RolesStore chronograf.RolesStore
Logger chronograf.Logger
dataNodes *ring.Ring
opened bool
}
// NewClientWithTimeSeries initializes a Client with a known set of TimeSeries.
func NewClientWithTimeSeries(lg chronograf.Logger, mu, username, password string, tls bool, series ...chronograf.TimeSeries) (*Client, error) {
metaURL, err := parseMetaURL(mu, tls)
if err != nil {
return nil, err
}
metaURL.User = url.UserPassword(username, password)
ctrl := NewMetaClient(metaURL)
c := &Client{
Ctrl: ctrl,
UsersStore: &UserStore{
Ctrl: ctrl,
Logger: lg,
},
RolesStore: &RolesStore{
Ctrl: ctrl,
Logger: lg,
},
}
c.dataNodes = ring.New(len(series))
for _, s := range series {
c.dataNodes.Value = s
c.dataNodes = c.dataNodes.Next()
}
return c, nil
}
// NewClientWithURL initializes an Enterprise client with a URL to a Meta Node.
// Acceptable URLs include host:port combinations as well as scheme://host:port
// varieties. TLS is used when the URL contains "https" or when the TLS
// parameter is set. The latter option is provided for host:port combinations
// Username and Password are used for Basic Auth
func NewClientWithURL(mu, username, password string, tls bool, lg chronograf.Logger) (*Client, error) {
metaURL, err := parseMetaURL(mu, tls)
if err != nil {
return nil, err
}
metaURL.User = url.UserPassword(username, password)
ctrl := NewMetaClient(metaURL)
return &Client{
Ctrl: ctrl,
UsersStore: &UserStore{
Ctrl: ctrl,
Logger: lg,
},
RolesStore: &RolesStore{
Ctrl: ctrl,
Logger: lg,
},
Logger: lg,
}, nil
}
// Connect prepares a Client to process queries. It must be called prior to calling Query
func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
c.opened = true
// return early if we already have dataNodes
if c.dataNodes != nil {
return nil
}
cluster, err := c.Ctrl.ShowCluster(ctx)
if err != nil {
return err
}
c.dataNodes = ring.New(len(cluster.DataNodes))
for _, dn := range cluster.DataNodes {
cl, err := influx.NewClient(dn.HTTPAddr, c.Logger)
if err != nil {
continue
} else {
c.dataNodes.Value = cl
c.dataNodes = c.dataNodes.Next()
}
}
return nil
}
// Query retrieves timeseries information pertaining to a specified query. It
// can be cancelled by using a provided context.
func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Response, error) {
if !c.opened {
return nil, chronograf.ErrUninitialized
}
return c.nextDataNode().Query(ctx, q)
}
// Users is the interface to the users within Influx Enterprise
func (c *Client) Users(context.Context) chronograf.UsersStore {
return c.UsersStore
}
// Roles provide a grouping of permissions given to a grouping of users
func (c *Client) Roles(ctx context.Context) (chronograf.RolesStore, error) {
return c.RolesStore, nil
}
// Allowances returns all Influx Enterprise permission strings
func (c *Client) Allowances(context.Context) chronograf.Allowances {
return chronograf.Allowances{
"NoPermissions",
"ViewAdmin",
"ViewChronograf",
"CreateDatabase",
"CreateUserAndRole",
"AddRemoveNode",
"DropDatabase",
"DropData",
"ReadData",
"WriteData",
"Rebalance",
"ManageShard",
"ManageContinuousQuery",
"ManageQuery",
"ManageSubscription",
"Monitor",
"CopyShard",
"KapacitorAPI",
"KapacitorConfigAPI",
}
}
// nextDataNode retrieves the next available data node
func (c *Client) nextDataNode() chronograf.TimeSeries {
c.dataNodes = c.dataNodes.Next()
return c.dataNodes.Value.(chronograf.TimeSeries)
}
// parseMetaURL constructs a url from either a host:port combination or a
// scheme://host:port combo. The optional TLS parameter takes precedence over
// any TLS preference found in the provided URL
func parseMetaURL(mu string, tls bool) (metaURL *url.URL, err error) {
if strings.Contains(mu, "http") {
metaURL, err = url.Parse(mu)
} else {
metaURL = &url.URL{
Scheme: "http",
Host: mu,
}
}
if tls {
metaURL.Scheme = "https"
}
return
}

View File

@ -0,0 +1,185 @@
package enterprise_test
import (
"context"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/enterprise"
"github.com/influxdata/chronograf/log"
)
func Test_Enterprise_FetchesDataNodes(t *testing.T) {
t.Parallel()
showClustersCalled := false
ctrl := &mockCtrl{
showCluster: func(ctx context.Context) (*enterprise.Cluster, error) {
showClustersCalled = true
return &enterprise.Cluster{}, nil
},
}
cl := &enterprise.Client{
Ctrl: ctrl,
}
bg := context.Background()
err := cl.Connect(bg, &chronograf.Source{})
if err != nil {
t.Fatal("Unexpected error while creating enterprise client. err:", err)
}
if showClustersCalled != true {
t.Fatal("Expected request to meta node but none was issued")
}
}
func Test_Enterprise_IssuesQueries(t *testing.T) {
t.Parallel()
called := false
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
called = true
if r.URL.Path != "/query" {
t.Fatal("Expected request to '/query' but was", r.URL.Path)
}
rw.Write([]byte(`{}`))
}))
defer ts.Close()
cl := &enterprise.Client{
Ctrl: NewMockControlClient(ts.URL),
Logger: log.New(log.DebugLevel),
}
err := cl.Connect(context.Background(), &chronograf.Source{})
if err != nil {
t.Fatal("Unexpected error initializing client: err:", err)
}
_, err = cl.Query(context.Background(), chronograf.Query{Command: "show shards", DB: "_internal", RP: "autogen"})
if err != nil {
t.Fatal("Unexpected error while querying data node: err:", err)
}
if called == false {
t.Fatal("Expected request to data node but none was received")
}
}
func Test_Enterprise_AdvancesDataNodes(t *testing.T) {
m1 := NewMockTimeSeries("http://host-1.example.com:8086")
m2 := NewMockTimeSeries("http://host-2.example.com:8086")
cl, err := enterprise.NewClientWithTimeSeries(log.New(log.DebugLevel), "http://meta.example.com:8091", "marty", "thelake", false, chronograf.TimeSeries(m1), chronograf.TimeSeries(m2))
if err != nil {
t.Error("Unexpected error while initializing client: err:", err)
}
err = cl.Connect(context.Background(), &chronograf.Source{})
if err != nil {
t.Error("Unexpected error while initializing client: err:", err)
}
_, err = cl.Query(context.Background(), chronograf.Query{Command: "show shards", DB: "_internal", RP: "autogen"})
if err != nil {
t.Fatal("Unexpected error while issuing query: err:", err)
}
_, err = cl.Query(context.Background(), chronograf.Query{Command: "show shards", DB: "_internal", RP: "autogen"})
if err != nil {
t.Fatal("Unexpected error while issuing query: err:", err)
}
if m1.QueryCtr != 1 || m2.QueryCtr != 1 {
t.Fatalf("Expected m1.Query to be called once but was %d. Expected m2.Query to be called once but was %d\n", m1.QueryCtr, m2.QueryCtr)
}
}
func Test_Enterprise_NewClientWithURL(t *testing.T) {
t.Parallel()
urls := []struct {
url string
username string
password string
tls bool
shouldErr bool
}{
{"http://localhost:8086", "", "", false, false},
{"https://localhost:8086", "", "", false, false},
{"http://localhost:8086", "username", "password", false, false},
{"http://localhost:8086", "", "", true, false},
{"https://localhost:8086", "", "", true, false},
{"localhost:8086", "", "", false, false},
{"localhost:8086", "", "", true, false},
{":http", "", "", false, true},
}
for _, testURL := range urls {
_, err := enterprise.NewClientWithURL(testURL.url, testURL.username, testURL.password, testURL.tls, log.New(log.DebugLevel))
if err != nil && !testURL.shouldErr {
t.Errorf("Unexpected error creating Client with URL %s and TLS preference %t. err: %s", testURL.url, testURL.tls, err.Error())
} else if err == nil && testURL.shouldErr {
t.Errorf("Expected error creating Client with URL %s and TLS preference %t", testURL.url, testURL.tls)
}
}
}
func Test_Enterprise_ComplainsIfNotOpened(t *testing.T) {
m1 := NewMockTimeSeries("http://host-1.example.com:8086")
cl, err := enterprise.NewClientWithTimeSeries(log.New(log.DebugLevel), "http://meta.example.com:8091", "docbrown", "1.21 gigawatts", false, chronograf.TimeSeries(m1))
if err != nil {
t.Error("Expected ErrUnitialized, but was this err:", err)
}
_, err = cl.Query(context.Background(), chronograf.Query{Command: "show shards", DB: "_internal", RP: "autogen"})
if err != chronograf.ErrUninitialized {
t.Error("Expected ErrUnitialized, but was this err:", err)
}
}
func TestClient_Allowances(t *testing.T) {
tests := []struct {
name string
want chronograf.Allowances
}{
{
name: "All possible enterprise permissions",
want: chronograf.Allowances{
"NoPermissions",
"ViewAdmin",
"ViewChronograf",
"CreateDatabase",
"CreateUserAndRole",
"AddRemoveNode",
"DropDatabase",
"DropData",
"ReadData",
"WriteData",
"Rebalance",
"ManageShard",
"ManageContinuousQuery",
"ManageQuery",
"ManageSubscription",
"Monitor",
"CopyShard",
"KapacitorAPI",
"KapacitorConfigAPI",
},
},
}
for _, tt := range tests {
c := &enterprise.Client{}
if got := c.Allowances(context.Background()); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Client.Allowances() = %v, want %v", tt.name, got, tt.want)
}
}
}

367
enterprise/meta.go Normal file
View File

@ -0,0 +1,367 @@
package enterprise
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"github.com/influxdata/chronograf"
)
// MetaClient represents a Meta node in an Influx Enterprise cluster
type MetaClient struct {
URL *url.URL
client interface {
Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error)
}
}
// NewMetaClient represents a meta node in an Influx Enterprise cluster
func NewMetaClient(url *url.URL) *MetaClient {
return &MetaClient{
URL: url,
client: &defaultClient{},
}
}
// ShowCluster returns the cluster configuration (not health)
func (m *MetaClient) ShowCluster(ctx context.Context) (*Cluster, error) {
res, err := m.Do(ctx, "GET", "/show-cluster", nil, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
out := &Cluster{}
err = dec.Decode(out)
if err != nil {
return nil, err
}
return out, nil
}
// Users gets all the users. If name is not nil it filters for a single user
func (m *MetaClient) Users(ctx context.Context, name *string) (*Users, error) {
params := map[string]string{}
if name != nil {
params["name"] = *name
}
res, err := m.Do(ctx, "GET", "/user", params, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
users := &Users{}
err = dec.Decode(users)
if err != nil {
return nil, err
}
return users, nil
}
// User returns a single Influx Enterprise user
func (m *MetaClient) User(ctx context.Context, name string) (*User, error) {
users, err := m.Users(ctx, &name)
if err != nil {
return nil, err
}
for _, user := range users.Users {
return &user, nil
}
return nil, fmt.Errorf("No user found")
}
// CreateUser adds a user to Influx Enterprise
func (m *MetaClient) CreateUser(ctx context.Context, name, passwd string) error {
return m.CreateUpdateUser(ctx, "create", name, passwd)
}
// ChangePassword updates a user's password in Influx Enterprise
func (m *MetaClient) ChangePassword(ctx context.Context, name, passwd string) error {
return m.CreateUpdateUser(ctx, "change-password", name, passwd)
}
// CreateUpdateUser is a helper function to POST to the /user Influx Enterprise endpoint
func (m *MetaClient) CreateUpdateUser(ctx context.Context, action, name, passwd string) error {
a := &UserAction{
Action: action,
User: &User{
Name: name,
Password: passwd,
},
}
return m.Post(ctx, "/user", a, nil)
}
// DeleteUser removes a user from Influx Enterprise
func (m *MetaClient) DeleteUser(ctx context.Context, name string) error {
a := &UserAction{
Action: "delete",
User: &User{
Name: name,
},
}
return m.Post(ctx, "/user", a, nil)
}
// RemoveAllUserPerms revokes all permissions for a user in Influx Enterprise
func (m *MetaClient) RemoveAllUserPerms(ctx context.Context, name string) error {
user, err := m.User(ctx, name)
if err != nil {
return err
}
// No permissions to remove
if len(user.Permissions) == 0 {
return nil
}
a := &UserAction{
Action: "remove-permissions",
User: user,
}
return m.Post(ctx, "/user", a, nil)
}
// SetUserPerms removes all permissions and then adds the requested perms
func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permissions) error {
err := m.RemoveAllUserPerms(ctx, name)
if err != nil {
return err
}
// No permissions to add, so, user is in the right state
if len(perms) == 0 {
return nil
}
a := &UserAction{
Action: "add-permissions",
User: &User{
Name: name,
Permissions: perms,
},
}
return m.Post(ctx, "/user", a, nil)
}
// Roles gets all the roles. If name is not nil it filters for a single role
func (m *MetaClient) Roles(ctx context.Context, name *string) (*Roles, error) {
params := map[string]string{}
if name != nil {
params["name"] = *name
}
res, err := m.Do(ctx, "GET", "/role", params, nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
roles := &Roles{}
err = dec.Decode(roles)
if err != nil {
return nil, err
}
return roles, nil
}
// Role returns a single named role
func (m *MetaClient) Role(ctx context.Context, name string) (*Role, error) {
roles, err := m.Roles(ctx, &name)
if err != nil {
return nil, err
}
for _, role := range roles.Roles {
return &role, nil
}
return nil, fmt.Errorf("No role found")
}
// CreateRole adds a role to Influx Enterprise
func (m *MetaClient) CreateRole(ctx context.Context, name string) error {
a := &RoleAction{
Action: "create",
Role: &Role{
Name: name,
},
}
return m.Post(ctx, "/role", a, nil)
}
// DeleteRole removes a role from Influx Enterprise
func (m *MetaClient) DeleteRole(ctx context.Context, name string) error {
a := &RoleAction{
Action: "delete",
Role: &Role{
Name: name,
},
}
return m.Post(ctx, "/role", a, nil)
}
// RemoveAllRolePerms removes all permissions from a role
func (m *MetaClient) RemoveAllRolePerms(ctx context.Context, name string) error {
role, err := m.Role(ctx, name)
if err != nil {
return err
}
// No permissions to remove
if len(role.Permissions) == 0 {
return nil
}
a := &RoleAction{
Action: "remove-permissions",
Role: role,
}
return m.Post(ctx, "/role", a, nil)
}
// SetRolePerms removes all permissions and then adds the requested perms to role
func (m *MetaClient) SetRolePerms(ctx context.Context, name string, perms Permissions) error {
err := m.RemoveAllRolePerms(ctx, name)
if err != nil {
return err
}
// No permissions to add, so, role is in the right state
if len(perms) == 0 {
return nil
}
a := &RoleAction{
Action: "add-permissions",
Role: &Role{
Name: name,
Permissions: perms,
},
}
return m.Post(ctx, "/role", a, nil)
}
// RemoveAllRoleUsers removes all users from a role
func (m *MetaClient) RemoveAllRoleUsers(ctx context.Context, name string) error {
role, err := m.Role(ctx, name)
if err != nil {
return err
}
// No users to remove
if len(role.Users) == 0 {
return nil
}
a := &RoleAction{
Action: "remove-users",
Role: role,
}
return m.Post(ctx, "/role", a, nil)
}
// SetRoleUsers removes all users and then adds the requested users to role
func (m *MetaClient) SetRoleUsers(ctx context.Context, name string, users []string) error {
err := m.RemoveAllRoleUsers(ctx, name)
if err != nil {
return err
}
// No permissions to add, so, role is in the right state
if len(users) == 0 {
return nil
}
a := &RoleAction{
Action: "add-users",
Role: &Role{
Name: name,
Users: users,
},
}
return m.Post(ctx, "/role", a, nil)
}
// Post is a helper function to POST to Influx Enterprise
func (m *MetaClient) Post(ctx context.Context, path string, action interface{}, params map[string]string) error {
b, err := json.Marshal(action)
if err != nil {
return err
}
body := bytes.NewReader(b)
_, err = m.Do(ctx, "POST", path, params, body)
if err != nil {
return err
}
return nil
}
type defaultClient struct{}
// Do is a helper function to interface with Influx Enterprise's Meta API
func (d *defaultClient) Do(URL *url.URL, path, method string, params map[string]string, body io.Reader) (*http.Response, error) {
p := url.Values{}
for k, v := range params {
p.Add(k, v)
}
URL.Path = path
URL.RawQuery = p.Encode()
req, err := http.NewRequest(method, URL.String(), body)
if err != nil {
return nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
defer res.Body.Close()
dec := json.NewDecoder(res.Body)
out := &Error{}
err = dec.Decode(out)
if err != nil {
return nil, err
}
return nil, errors.New(out.Error)
}
return res, nil
}
// Do is a cancelable function to interface with Influx Enterprise's Meta API
func (m *MetaClient) Do(ctx context.Context, method, path string, params map[string]string, body io.Reader) (*http.Response, error) {
type result struct {
Response *http.Response
Err error
}
resps := make(chan (result))
go func() {
resp, err := m.client.Do(m.URL, path, method, params, body)
resps <- result{resp, err}
}()
select {
case resp := <-resps:
return resp.Response, resp.Err
case <-ctx.Done():
return nil, chronograf.ErrUpstreamTimeout
}
}

1307
enterprise/meta_test.go Normal file

File diff suppressed because it is too large Load Diff

126
enterprise/mocks_test.go Normal file
View File

@ -0,0 +1,126 @@
package enterprise_test
import (
"context"
"encoding/json"
"net/url"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/enterprise"
)
type ControlClient struct {
Cluster *enterprise.Cluster
ShowClustersCalled bool
}
func NewMockControlClient(addr string) *ControlClient {
_, err := url.Parse(addr)
if err != nil {
panic(err)
}
return &ControlClient{
Cluster: &enterprise.Cluster{
DataNodes: []enterprise.DataNode{
enterprise.DataNode{
HTTPAddr: addr,
},
},
},
}
}
func (cc *ControlClient) ShowCluster(context.Context) (*enterprise.Cluster, error) {
cc.ShowClustersCalled = true
return cc.Cluster, nil
}
func (cc *ControlClient) User(ctx context.Context, name string) (*enterprise.User, error) {
return nil, nil
}
func (cc *ControlClient) CreateUser(ctx context.Context, name, passwd string) error {
return nil
}
func (cc *ControlClient) DeleteUser(ctx context.Context, name string) error {
return nil
}
func (cc *ControlClient) ChangePassword(ctx context.Context, name, passwd string) error {
return nil
}
func (cc *ControlClient) Users(ctx context.Context, name *string) (*enterprise.Users, error) {
return nil, nil
}
func (cc *ControlClient) SetUserPerms(ctx context.Context, name string, perms enterprise.Permissions) error {
return nil
}
func (cc *ControlClient) CreateRole(ctx context.Context, name string) error {
return nil
}
func (cc *ControlClient) Role(ctx context.Context, name string) (*enterprise.Role, error) {
return nil, nil
}
func (ccm *ControlClient) Roles(ctx context.Context, name *string) (*enterprise.Roles, error) {
return nil, nil
}
func (cc *ControlClient) DeleteRole(ctx context.Context, name string) error {
return nil
}
func (cc *ControlClient) SetRolePerms(ctx context.Context, name string, perms enterprise.Permissions) error {
return nil
}
func (cc *ControlClient) SetRoleUsers(ctx context.Context, name string, users []string) error {
return nil
}
type TimeSeries struct {
URLs []string
Response Response
QueryCtr int
}
type Response struct{}
func (r *Response) MarshalJSON() ([]byte, error) {
return json.Marshal(r)
}
func (ts *TimeSeries) Query(ctx context.Context, q chronograf.Query) (chronograf.Response, error) {
ts.QueryCtr++
return &Response{}, nil
}
func (ts *TimeSeries) Connect(ctx context.Context, src *chronograf.Source) error {
return nil
}
func (ts *TimeSeries) Users(ctx context.Context) chronograf.UsersStore {
return nil
}
func (ts *TimeSeries) Roles(ctx context.Context) (chronograf.RolesStore, error) {
return nil, nil
}
func (ts *TimeSeries) Allowances(ctx context.Context) chronograf.Allowances {
return chronograf.Allowances{}
}
func NewMockTimeSeries(urls ...string) *TimeSeries {
return &TimeSeries{
URLs: urls,
Response: Response{},
}
}

105
enterprise/roles.go Normal file
View File

@ -0,0 +1,105 @@
package enterprise
import (
"context"
"github.com/influxdata/chronograf"
)
// RolesStore uses a control client operate on Influx Enterprise roles. Roles are
// groups of permissions applied to groups of users
type RolesStore struct {
Ctrl
Logger chronograf.Logger
}
// Add creates a new Role in Influx Enterprise
// This must be done in three smaller steps: creating, setting permissions, setting users.
func (c *RolesStore) Add(ctx context.Context, u *chronograf.Role) (*chronograf.Role, error) {
if err := c.Ctrl.CreateRole(ctx, u.Name); err != nil {
return nil, err
}
if err := c.Ctrl.SetRolePerms(ctx, u.Name, ToEnterprise(u.Permissions)); err != nil {
return nil, err
}
users := make([]string, len(u.Users))
for i, u := range u.Users {
users[i] = u.Name
}
if err := c.Ctrl.SetRoleUsers(ctx, u.Name, users); err != nil {
return nil, err
}
return u, nil
}
// Delete the Role from Influx Enterprise
func (c *RolesStore) Delete(ctx context.Context, u *chronograf.Role) error {
return c.Ctrl.DeleteRole(ctx, u.Name)
}
// Get retrieves a Role if name exists.
func (c *RolesStore) Get(ctx context.Context, name string) (*chronograf.Role, error) {
role, err := c.Ctrl.Role(ctx, name)
if err != nil {
return nil, err
}
// Hydrate all the users to gather their permissions and their roles.
users := make([]chronograf.User, len(role.Users))
for i, u := range role.Users {
user, err := c.Ctrl.User(ctx, u)
if err != nil {
return nil, err
}
users[i] = chronograf.User{
Name: user.Name,
Permissions: ToChronograf(user.Permissions),
}
}
return &chronograf.Role{
Name: role.Name,
Permissions: ToChronograf(role.Permissions),
Users: users,
}, nil
}
// Update the Role's permissions and roles
func (c *RolesStore) Update(ctx context.Context, u *chronograf.Role) error {
perms := ToEnterprise(u.Permissions)
if err := c.Ctrl.SetRolePerms(ctx, u.Name, perms); err != nil {
return err
}
users := make([]string, len(u.Users))
for i, u := range u.Users {
users[i] = u.Name
}
return c.Ctrl.SetRoleUsers(ctx, u.Name, users)
}
// All is all Roles in influx
func (c *RolesStore) All(ctx context.Context) ([]chronograf.Role, error) {
all, err := c.Ctrl.Roles(ctx, nil)
if err != nil {
return nil, err
}
res := make([]chronograf.Role, len(all.Roles))
for i, role := range all.Roles {
users := make([]chronograf.User, len(role.Users))
for i, user := range role.Users {
users[i] = chronograf.User{
Name: user,
}
}
res[i] = chronograf.Role{
Name: role.Name,
Permissions: ToChronograf(role.Permissions),
Users: users,
}
}
return res, nil
}

71
enterprise/types.go Normal file
View File

@ -0,0 +1,71 @@
package enterprise
// Cluster is a collection of data nodes and non-data nodes within a
// Plutonium cluster.
type Cluster struct {
DataNodes []DataNode `json:"data"`
MetaNodes []Node `json:"meta"`
}
// DataNode represents a data node in an Influx Enterprise Cluster
type DataNode struct {
ID uint64 `json:"id"` // Meta store ID.
TCPAddr string `json:"tcpAddr"` // RPC addr, e.g., host:8088.
HTTPAddr string `json:"httpAddr"` // Client addr, e.g., host:8086.
HTTPScheme string `json:"httpScheme"` // "http" or "https" for HTTP addr.
Status string `json:"status,omitempty"` // The cluster status of the node.
}
// Node represent any meta or data node in an Influx Enterprise cluster
type Node struct {
ID uint64 `json:"id"`
Addr string `json:"addr"`
HTTPScheme string `json:"httpScheme"`
TCPAddr string `json:"tcpAddr"`
}
// Permissions maps resources to a set of permissions.
// Specifically, it maps a database to a set of permissions
type Permissions map[string][]string
// User represents an enterprise user.
type User struct {
Name string `json:"name"`
Password string `json:"password,omitempty"`
Permissions Permissions `json:"permissions,omitempty"`
}
// Users represents a set of enterprise users.
type Users struct {
Users []User `json:"users,omitempty"`
}
// UserAction represents and action to be taken with a user.
type UserAction struct {
Action string `json:"action"`
User *User `json:"user"`
}
// Role is a restricted set of permissions assigned to a set of users.
type Role struct {
Name string `json:"name"`
NewName string `json:"newName,omitempty"`
Permissions Permissions `json:"permissions,omitempty"`
Users []string `json:"users,omitempty"`
}
// Roles is a set of roles
type Roles struct {
Roles []Role `json:"roles,omitempty"`
}
// RoleAction represents an action to be taken with a role.
type RoleAction struct {
Action string `json:"action"`
Role *Role `json:"role"`
}
// Error is JSON error message return by Influx Enterprise's meta API.
type Error struct {
Error string `json:"error"`
}

106
enterprise/users.go Normal file
View File

@ -0,0 +1,106 @@
package enterprise
import (
"context"
"github.com/influxdata/chronograf"
)
// UserStore uses a control client operate on Influx Enterprise users
type UserStore struct {
Ctrl
Logger chronograf.Logger
}
// Add creates a new User in Influx Enterprise
func (c *UserStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
if err := c.Ctrl.CreateUser(ctx, u.Name, u.Passwd); err != nil {
return nil, err
}
perms := ToEnterprise(u.Permissions)
if err := c.Ctrl.SetUserPerms(ctx, u.Name, perms); err != nil {
return nil, err
}
return u, nil
}
// Delete the User from Influx Enterprise
func (c *UserStore) Delete(ctx context.Context, u *chronograf.User) error {
return c.Ctrl.DeleteUser(ctx, u.Name)
}
// Get retrieves a user if name exists.
func (c *UserStore) Get(ctx context.Context, name string) (*chronograf.User, error) {
u, err := c.Ctrl.User(ctx, name)
if err != nil {
return nil, err
}
return &chronograf.User{
Name: u.Name,
Permissions: ToChronograf(u.Permissions),
}, nil
}
// Update the user's permissions or roles
func (c *UserStore) Update(ctx context.Context, u *chronograf.User) error {
// Only allow one type of change at a time. If it is a password
// change then do it and return without any changes to permissions
if u.Passwd != "" {
return c.Ctrl.ChangePassword(ctx, u.Name, u.Passwd)
}
perms := ToEnterprise(u.Permissions)
return c.Ctrl.SetUserPerms(ctx, u.Name, perms)
}
// All is all users in influx
func (c *UserStore) All(ctx context.Context) ([]chronograf.User, error) {
all, err := c.Ctrl.Users(ctx, nil)
if err != nil {
return nil, err
}
res := make([]chronograf.User, len(all.Users))
for i, user := range all.Users {
res[i] = chronograf.User{
Name: user.Name,
Permissions: ToChronograf(user.Permissions),
}
}
return res, nil
}
// ToEnterprise converts chronograf permission shape to enterprise
func ToEnterprise(perms chronograf.Permissions) Permissions {
res := Permissions{}
for _, perm := range perms {
if perm.Scope == chronograf.AllScope {
// Enterprise uses empty string as the key for all databases
res[""] = perm.Allowed
} else {
res[perm.Name] = perm.Allowed
}
}
return res
}
// ToChronograf converts enterprise permissions shape to chronograf shape
func ToChronograf(perms Permissions) chronograf.Permissions {
res := chronograf.Permissions{}
for db, perm := range perms {
// Enterprise uses empty string as the key for all databases
if db == "" {
res = append(res, chronograf.Permission{
Scope: chronograf.AllScope,
Allowed: perm,
})
} else {
res = append(res, chronograf.Permission{
Scope: chronograf.DBScope,
Name: db,
Allowed: perm,
})
}
}
return res
}

554
enterprise/users_test.go Normal file
View File

@ -0,0 +1,554 @@
package enterprise_test
import (
"context"
"fmt"
"reflect"
"testing"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/enterprise"
)
func TestClient_Add(t *testing.T) {
type fields struct {
Ctrl *mockCtrl
Logger chronograf.Logger
}
type args struct {
ctx context.Context
u *chronograf.User
}
tests := []struct {
name string
fields fields
args args
want *chronograf.User
wantErr bool
}{
{
name: "Successful Create User",
fields: fields{
Ctrl: &mockCtrl{
createUser: func(ctx context.Context, name, passwd string) error {
return nil
},
setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error {
return nil
},
},
},
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "marty",
Passwd: "johnny be good",
},
},
want: &chronograf.User{
Name: "marty",
Passwd: "johnny be good",
},
},
{
name: "Failure to Create User",
fields: fields{
Ctrl: &mockCtrl{
createUser: func(ctx context.Context, name, passwd string) error {
return fmt.Errorf("1.21 Gigawatts! Tom, how could I have been so careless?")
},
},
},
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "marty",
Passwd: "johnny be good",
},
},
wantErr: true,
},
}
for _, tt := range tests {
c := &enterprise.UserStore{
Ctrl: tt.fields.Ctrl,
Logger: tt.fields.Logger,
}
got, err := c.Add(tt.args.ctx, tt.args.u)
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Client.Add() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestClient_Delete(t *testing.T) {
type fields struct {
Ctrl *mockCtrl
Logger chronograf.Logger
}
type args struct {
ctx context.Context
u *chronograf.User
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "Successful Delete User",
fields: fields{
Ctrl: &mockCtrl{
deleteUser: func(ctx context.Context, name string) error {
return nil
},
},
},
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "marty",
Passwd: "johnny be good",
},
},
},
{
name: "Failure to Delete User",
fields: fields{
Ctrl: &mockCtrl{
deleteUser: func(ctx context.Context, name string) error {
return fmt.Errorf("1.21 Gigawatts! Tom, how could I have been so careless?")
},
},
},
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "marty",
Passwd: "johnny be good",
},
},
wantErr: true,
},
}
for _, tt := range tests {
c := &enterprise.UserStore{
Ctrl: tt.fields.Ctrl,
Logger: tt.fields.Logger,
}
if err := c.Delete(tt.args.ctx, tt.args.u); (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
}
}
func TestClient_Get(t *testing.T) {
type fields struct {
Ctrl *mockCtrl
Logger chronograf.Logger
}
type args struct {
ctx context.Context
name string
}
tests := []struct {
name string
fields fields
args args
want *chronograf.User
wantErr bool
}{
{
name: "Successful Get User",
fields: fields{
Ctrl: &mockCtrl{
user: func(ctx context.Context, name string) (*enterprise.User, error) {
return &enterprise.User{
Name: "marty",
Password: "johnny be good",
Permissions: map[string][]string{
"": {
"ViewChronograf",
"ReadData",
"WriteData",
},
},
}, nil
},
},
},
args: args{
ctx: context.Background(),
name: "marty",
},
want: &chronograf.User{
Name: "marty",
Permissions: chronograf.Permissions{
{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"},
},
},
},
},
{
name: "Failure to get User",
fields: fields{
Ctrl: &mockCtrl{
user: func(ctx context.Context, name string) (*enterprise.User, error) {
return nil, fmt.Errorf("1.21 Gigawatts! Tom, how could I have been so careless?")
},
},
},
args: args{
ctx: context.Background(),
name: "marty",
},
wantErr: true,
},
}
for _, tt := range tests {
c := &enterprise.UserStore{
Ctrl: tt.fields.Ctrl,
Logger: tt.fields.Logger,
}
got, err := c.Get(tt.args.ctx, tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Client.Get() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestClient_Update(t *testing.T) {
type fields struct {
Ctrl *mockCtrl
Logger chronograf.Logger
}
type args struct {
ctx context.Context
u *chronograf.User
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "Successful Change Password",
fields: fields{
Ctrl: &mockCtrl{
changePassword: func(ctx context.Context, name, passwd string) error {
return nil
},
},
},
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "marty",
Passwd: "johnny be good",
},
},
},
{
name: "Failure to Change Password",
fields: fields{
Ctrl: &mockCtrl{
changePassword: func(ctx context.Context, name, passwd string) error {
return fmt.Errorf("Ronald Reagan, the actor?! Ha Then whos Vice President Jerry Lewis? I suppose Jane Wyman is First Lady")
},
},
},
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "marty",
Passwd: "johnny be good",
},
},
wantErr: true,
},
{
name: "Success setting permissions User",
fields: fields{
Ctrl: &mockCtrl{
setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error {
return nil
},
},
},
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "marty",
Permissions: chronograf.Permissions{
{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"ViewChronograf", "KapacitorAPI"},
},
},
},
},
wantErr: false,
},
{
name: "Failure setting permissions User",
fields: fields{
Ctrl: &mockCtrl{
setUserPerms: func(ctx context.Context, name string, perms enterprise.Permissions) error {
return fmt.Errorf("They found me, I don't know how, but they found me.")
},
},
},
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "marty",
Permissions: chronograf.Permissions{
{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"ViewChronograf", "KapacitorAPI"},
},
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
c := &enterprise.UserStore{
Ctrl: tt.fields.Ctrl,
Logger: tt.fields.Logger,
}
if err := c.Update(tt.args.ctx, tt.args.u); (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
}
}
func TestClient_All(t *testing.T) {
type fields struct {
Ctrl *mockCtrl
Logger chronograf.Logger
}
type args struct {
ctx context.Context
}
tests := []struct {
name string
fields fields
args args
want []chronograf.User
wantErr bool
}{
{
name: "Successful Get User",
fields: fields{
Ctrl: &mockCtrl{
users: func(ctx context.Context, name *string) (*enterprise.Users, error) {
return &enterprise.Users{
Users: []enterprise.User{
{
Name: "marty",
Password: "johnny be good",
Permissions: map[string][]string{
"": {
"ViewChronograf",
"ReadData",
"WriteData",
},
},
},
},
}, nil
},
},
},
args: args{
ctx: context.Background(),
},
want: []chronograf.User{
{
Name: "marty",
Permissions: chronograf.Permissions{
{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"ViewChronograf", "ReadData", "WriteData"},
},
},
},
},
},
{
name: "Failure to get User",
fields: fields{
Ctrl: &mockCtrl{
users: func(ctx context.Context, name *string) (*enterprise.Users, error) {
return nil, fmt.Errorf("1.21 Gigawatts! Tom, how could I have been so careless?")
},
},
},
args: args{
ctx: context.Background(),
},
wantErr: true,
},
}
for _, tt := range tests {
c := &enterprise.UserStore{
Ctrl: tt.fields.Ctrl,
Logger: tt.fields.Logger,
}
got, err := c.All(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Client.All() = %v, want %v", tt.name, got, tt.want)
}
}
}
func Test_ToEnterprise(t *testing.T) {
tests := []struct {
name string
perms chronograf.Permissions
want enterprise.Permissions
}{
{
name: "All Scopes",
want: enterprise.Permissions{"": []string{"ViewChronograf", "KapacitorAPI"}},
perms: chronograf.Permissions{
{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"ViewChronograf", "KapacitorAPI"},
},
},
},
{
name: "DB Scope",
want: enterprise.Permissions{"telegraf": []string{"ReadData", "WriteData"}},
perms: chronograf.Permissions{
{
Scope: chronograf.DBScope,
Name: "telegraf",
Allowed: chronograf.Allowances{"ReadData", "WriteData"},
},
},
},
}
for _, tt := range tests {
if got := enterprise.ToEnterprise(tt.perms); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. ToEnterprise() = %v, want %v", tt.name, got, tt.want)
}
}
}
func Test_ToChronograf(t *testing.T) {
tests := []struct {
name string
perms enterprise.Permissions
want chronograf.Permissions
}{
{
name: "All Scopes",
perms: enterprise.Permissions{"": []string{"ViewChronograf", "KapacitorAPI"}},
want: chronograf.Permissions{
{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"ViewChronograf", "KapacitorAPI"},
},
},
},
{
name: "DB Scope",
perms: enterprise.Permissions{"telegraf": []string{"ReadData", "WriteData"}},
want: chronograf.Permissions{
{
Scope: chronograf.DBScope,
Name: "telegraf",
Allowed: chronograf.Allowances{"ReadData", "WriteData"},
},
},
},
}
for _, tt := range tests {
if got := enterprise.ToChronograf(tt.perms); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. toChronograf() = %v, want %v", tt.name, got, tt.want)
}
}
}
type mockCtrl struct {
showCluster func(ctx context.Context) (*enterprise.Cluster, error)
user func(ctx context.Context, name string) (*enterprise.User, error)
createUser func(ctx context.Context, name, passwd string) error
deleteUser func(ctx context.Context, name string) error
changePassword func(ctx context.Context, name, passwd string) error
users func(ctx context.Context, name *string) (*enterprise.Users, error)
setUserPerms func(ctx context.Context, name string, perms enterprise.Permissions) error
roles func(ctx context.Context, name *string) (*enterprise.Roles, error)
role func(ctx context.Context, name string) (*enterprise.Role, error)
createRole func(ctx context.Context, name string) error
deleteRole func(ctx context.Context, name string) error
setRolePerms func(ctx context.Context, name string, perms enterprise.Permissions) error
setRoleUsers func(ctx context.Context, name string, users []string) error
}
func (m *mockCtrl) ShowCluster(ctx context.Context) (*enterprise.Cluster, error) {
return m.showCluster(ctx)
}
func (m *mockCtrl) User(ctx context.Context, name string) (*enterprise.User, error) {
return m.user(ctx, name)
}
func (m *mockCtrl) CreateUser(ctx context.Context, name, passwd string) error {
return m.createUser(ctx, name, passwd)
}
func (m *mockCtrl) DeleteUser(ctx context.Context, name string) error {
return m.deleteUser(ctx, name)
}
func (m *mockCtrl) ChangePassword(ctx context.Context, name, passwd string) error {
return m.changePassword(ctx, name, passwd)
}
func (m *mockCtrl) Users(ctx context.Context, name *string) (*enterprise.Users, error) {
return m.users(ctx, name)
}
func (m *mockCtrl) SetUserPerms(ctx context.Context, name string, perms enterprise.Permissions) error {
return m.setUserPerms(ctx, name, perms)
}
func (m *mockCtrl) Roles(ctx context.Context, name *string) (*enterprise.Roles, error) {
return m.roles(ctx, name)
}
func (m *mockCtrl) Role(ctx context.Context, name string) (*enterprise.Role, error) {
return m.role(ctx, name)
}
func (m *mockCtrl) CreateRole(ctx context.Context, name string) error {
return m.createRole(ctx, name)
}
func (m *mockCtrl) DeleteRole(ctx context.Context, name string) error {
return m.deleteRole(ctx, name)
}
func (m *mockCtrl) SetRolePerms(ctx context.Context, name string, perms enterprise.Permissions) error {
return m.setRolePerms(ctx, name, perms)
}
func (m *mockCtrl) SetRoleUsers(ctx context.Context, name string, users []string) error {
return m.setRoleUsers(ctx, name, users)
}

View File

@ -11,6 +11,8 @@ import (
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
) )
var _ chronograf.TimeSeries = &Client{}
// Client is a device for retrieving time series data from an InfluxDB instance // Client is a device for retrieving time series data from an InfluxDB instance
type Client struct { type Client struct {
URL *url.URL URL *url.URL
@ -35,11 +37,14 @@ func NewClient(host string, lg chronograf.Logger) (*Client, error) {
}, nil }, nil
} }
// Response is a partial JSON decoded InfluxQL response used
// to check for some errors
type Response struct { type Response struct {
Results json.RawMessage Results json.RawMessage
Err string `json:"error,omitempty"` Err string `json:"error,omitempty"`
} }
// MarshalJSON returns the raw results bytes from the response
func (r Response) MarshalJSON() ([]byte, error) { func (r Response) MarshalJSON() ([]byte, error) {
return r.Results, nil return r.Results, nil
} }
@ -148,6 +153,7 @@ func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Resp
} }
} }
// Connect caches the URL for the data source
func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error { func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
u, err := url.Parse(src.URL) u, err := url.Parse(src.URL)
if err != nil { if err != nil {
@ -161,3 +167,13 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
c.URL = u c.URL = u
return nil return nil
} }
// Users transforms InfluxDB into a user store
func (c *Client) Users(ctx context.Context) chronograf.UsersStore {
return c
}
// Roles aren't support in OSS
func (c *Client) Roles(ctx context.Context) (chronograf.RolesStore, error) {
return nil, fmt.Errorf("Roles not support in open-source InfluxDB. Roles are support in Influx Enterprise")
}

View File

@ -1,6 +1,7 @@
package influx_test package influx_test
import ( import (
"context"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -9,7 +10,6 @@ import (
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx" "github.com/influxdata/chronograf/influx"
"github.com/influxdata/chronograf/log" "github.com/influxdata/chronograf/log"
"golang.org/x/net/context"
) )
func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) { func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
@ -204,3 +204,11 @@ func Test_Influx_ReportsInfluxErrs(t *testing.T) {
t.Fatal("Expected an error but received none") t.Fatal("Expected an error but received none")
} }
} }
func TestClient_Roles(t *testing.T) {
c := &influx.Client{}
_, err := c.Roles(context.Background())
if err == nil {
t.Errorf("Client.Roles() want error")
}
}

200
influx/permissions.go Normal file
View File

@ -0,0 +1,200 @@
package influx
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
var (
// AllowAll means a user gets both read and write permissions
AllowAll = chronograf.Allowances{"WRITE", "READ"}
// AllowRead means a user is only able to read the database.
AllowRead = chronograf.Allowances{"READ"}
// AllowWrite means a user is able to only write to the database
AllowWrite = chronograf.Allowances{"WRITE"}
// NoPrivileges occasionally shows up as a response for a users grants.
NoPrivileges = "NO PRIVILEGES"
// AllPrivileges means that a user has both read and write perms
AllPrivileges = "ALL PRIVILEGES"
// All means a user has both read and write perms. Alternative to AllPrivileges
All = "ALL"
// Read means a user can read a database
Read = "READ"
// Write means a user can write to a database
Write = "WRITE"
)
// Allowances return just READ and WRITE for OSS Influx
func (c *Client) Allowances(context.Context) chronograf.Allowances {
return chronograf.Allowances{"READ", "WRITE"}
}
// showResults is used to deserialize InfluxQL SHOW commands
type showResults []struct {
Series []struct {
Values [][]interface{} `json:"values"`
} `json:"series"`
}
// Users converts SHOW USERS to chronograf Users
func (r *showResults) Users() []chronograf.User {
res := []chronograf.User{}
for _, u := range *r {
for _, s := range u.Series {
for _, v := range s.Values {
if name, ok := v[0].(string); !ok {
continue
} else if admin, ok := v[1].(bool); !ok {
continue
} else {
c := chronograf.User{
Name: name,
Permissions: chronograf.Permissions{},
}
if admin {
c.Permissions = adminPerms()
}
res = append(res, c)
}
}
}
}
return res
}
// Permissions converts SHOW GRANTS to chronograf.Permissions
func (r *showResults) Permissions() chronograf.Permissions {
res := []chronograf.Permission{}
for _, u := range *r {
for _, s := range u.Series {
for _, v := range s.Values {
if db, ok := v[0].(string); !ok {
continue
} else if priv, ok := v[1].(string); !ok {
continue
} else {
c := chronograf.Permission{
Name: db,
Scope: chronograf.DBScope,
}
switch priv {
case AllPrivileges, All:
c.Allowed = AllowAll
case Read:
c.Allowed = AllowRead
case Write:
c.Allowed = AllowWrite
default:
// sometimes influx reports back NO PRIVILEGES
continue
}
res = append(res, c)
}
}
}
}
return res
}
func adminPerms() chronograf.Permissions {
return []chronograf.Permission{
{
Scope: chronograf.AllScope,
Allowed: AllowAll,
},
}
}
// ToInfluxQL converts the permission into InfluxQL
func ToInfluxQL(action, preposition, username string, perm chronograf.Permission) string {
if perm.Scope == chronograf.AllScope {
return fmt.Sprintf(`%s ALL PRIVILEGES %s "%s"`, action, preposition, username)
} else if len(perm.Allowed) == 0 {
// All privileges are to be removed for this user on this database
return fmt.Sprintf(`%s ALL PRIVILEGES ON "%s" %s "%s"`, action, perm.Name, preposition, username)
}
priv := ToPriv(perm.Allowed)
if priv == NoPrivileges {
return ""
}
return fmt.Sprintf(`%s %s ON "%s" %s "%s"`, action, priv, perm.Name, preposition, username)
}
// ToRevoke converts the permission into InfluxQL revokes
func ToRevoke(username string, perm chronograf.Permission) string {
return ToInfluxQL("REVOKE", "FROM", username, perm)
}
// ToGrant converts the permission into InfluxQL grants
func ToGrant(username string, perm chronograf.Permission) string {
if len(perm.Allowed) == 0 {
return ""
}
return ToInfluxQL("GRANT", "TO", username, perm)
}
// ToPriv converts chronograf allowances to InfluxQL
func ToPriv(a chronograf.Allowances) string {
if len(a) == 0 {
return NoPrivileges
}
hasWrite := false
hasRead := false
for _, aa := range a {
if aa == Read {
hasRead = true
} else if aa == Write {
hasWrite = true
} else if aa == All {
hasRead, hasWrite = true, true
}
}
if hasWrite && hasRead {
return All
} else if hasWrite {
return Write
} else if hasRead {
return Read
}
return NoPrivileges
}
// Difference compares two permission sets and returns a set to be revoked and a set to be added
func Difference(wants chronograf.Permissions, haves chronograf.Permissions) (revoke chronograf.Permissions, add chronograf.Permissions) {
for _, want := range wants {
found := false
for _, got := range haves {
if want.Scope != got.Scope || want.Name != got.Name {
continue
}
found = true
if len(want.Allowed) == 0 {
revoke = append(revoke, want)
} else {
add = append(add, want)
}
break
}
if !found {
add = append(add, want)
}
}
for _, got := range haves {
found := false
for _, want := range wants {
if want.Scope != got.Scope || want.Name != got.Name {
continue
}
found = true
break
}
if !found {
revoke = append(revoke, got)
}
}
return
}

422
influx/permissions_test.go Normal file
View File

@ -0,0 +1,422 @@
package influx
import (
"encoding/json"
"reflect"
"testing"
"github.com/influxdata/chronograf"
)
func TestDifference(t *testing.T) {
t.Parallel()
type args struct {
wants chronograf.Permissions
haves chronograf.Permissions
}
tests := []struct {
name string
args args
wantRevoke chronograf.Permissions
wantAdd chronograf.Permissions
}{
{
name: "add write to permissions",
args: args{
wants: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"READ", "WRITE"},
},
},
haves: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"READ"},
},
},
},
wantRevoke: nil,
wantAdd: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"READ", "WRITE"},
},
},
},
{
name: "revoke write to permissions",
args: args{
wants: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"READ"},
},
},
haves: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"READ", "WRITE"},
},
},
},
wantRevoke: nil,
wantAdd: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"READ"},
},
},
},
{
name: "revoke all permissions",
args: args{
wants: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{},
},
},
haves: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"READ", "WRITE"},
},
},
},
wantRevoke: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{},
},
},
wantAdd: nil,
},
{
name: "add permissions different db",
args: args{
wants: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "new",
Allowed: []string{"READ"},
},
},
haves: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "old",
Allowed: []string{"READ", "WRITE"},
},
},
},
wantRevoke: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "old",
Allowed: []string{"READ", "WRITE"},
},
},
wantAdd: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "new",
Allowed: []string{"READ"},
},
},
},
}
for _, tt := range tests {
gotRevoke, gotAdd := Difference(tt.args.wants, tt.args.haves)
if !reflect.DeepEqual(gotRevoke, tt.wantRevoke) {
t.Errorf("%q. Difference() gotRevoke = %v, want %v", tt.name, gotRevoke, tt.wantRevoke)
}
if !reflect.DeepEqual(gotAdd, tt.wantAdd) {
t.Errorf("%q. Difference() gotAdd = %v, want %v", tt.name, gotAdd, tt.wantAdd)
}
}
}
func TestToPriv(t *testing.T) {
t.Parallel()
type args struct {
a chronograf.Allowances
}
tests := []struct {
name string
args args
want string
}{
{
name: "no privs",
args: args{
a: chronograf.Allowances{},
},
want: NoPrivileges,
},
{
name: "read and write privs",
args: args{
a: chronograf.Allowances{"READ", "WRITE"},
},
want: All,
},
{
name: "write privs",
args: args{
a: chronograf.Allowances{"WRITE"},
},
want: Write,
},
{
name: "read privs",
args: args{
a: chronograf.Allowances{"READ"},
},
want: Read,
},
{
name: "all privs",
args: args{
a: chronograf.Allowances{"ALL"},
},
want: All,
},
{
name: "bad privs",
args: args{
a: chronograf.Allowances{"BAD"},
},
want: NoPrivileges,
},
}
for _, tt := range tests {
if got := ToPriv(tt.args.a); got != tt.want {
t.Errorf("%q. ToPriv() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestToGrant(t *testing.T) {
t.Parallel()
type args struct {
username string
perm chronograf.Permission
}
tests := []struct {
name string
args args
want string
}{
{
name: "grant all for all dbs",
args: args{
username: "biff",
perm: chronograf.Permission{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"ALL"},
},
},
want: `GRANT ALL PRIVILEGES TO "biff"`,
},
{
name: "grant all for one db",
args: args{
username: "biff",
perm: chronograf.Permission{
Scope: chronograf.DBScope,
Name: "gray_sports_almanac",
Allowed: chronograf.Allowances{"ALL"},
},
},
want: `GRANT ALL ON "gray_sports_almanac" TO "biff"`,
},
{
name: "bad allowance",
args: args{
username: "biff",
perm: chronograf.Permission{
Scope: chronograf.DBScope,
Name: "gray_sports_almanac",
Allowed: chronograf.Allowances{"bad"},
},
},
want: "",
},
}
for _, tt := range tests {
if got := ToGrant(tt.args.username, tt.args.perm); got != tt.want {
t.Errorf("%q. ToGrant() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestToRevoke(t *testing.T) {
t.Parallel()
type args struct {
username string
perm chronograf.Permission
}
tests := []struct {
name string
args args
want string
}{
{
name: "revoke all for all dbs",
args: args{
username: "biff",
perm: chronograf.Permission{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"ALL"},
},
},
want: `REVOKE ALL PRIVILEGES FROM "biff"`,
},
{
name: "revoke all for one db",
args: args{
username: "biff",
perm: chronograf.Permission{
Scope: chronograf.DBScope,
Name: "pleasure_paradice",
Allowed: chronograf.Allowances{},
},
},
want: `REVOKE ALL PRIVILEGES ON "pleasure_paradice" FROM "biff"`,
},
}
for _, tt := range tests {
if got := ToRevoke(tt.args.username, tt.args.perm); got != tt.want {
t.Errorf("%q. ToRevoke() = %v, want %v", tt.name, got, tt.want)
}
}
}
func Test_showResults_Users(t *testing.T) {
t.Parallel()
tests := []struct {
name string
octets []byte
want []chronograf.User
}{
{
name: "admin and non-admin",
octets: []byte(`[{"series":[{"columns":["user","admin"],"values":[["admin",true],["reader",false]]}]}]`),
want: []chronograf.User{
{
Name: "admin",
Permissions: chronograf.Permissions{
{
Scope: chronograf.AllScope,
Allowed: chronograf.Allowances{"WRITE", "READ"},
},
},
},
{
Name: "reader",
Permissions: chronograf.Permissions{},
},
},
},
{
name: "bad JSON",
octets: []byte(`[{"series":[{"columns":["user","admin"],"values":[[1,true],["reader","false"]]}]}]`),
want: []chronograf.User{},
},
}
for _, tt := range tests {
r := &showResults{}
json.Unmarshal(tt.octets, r)
if got := r.Users(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. showResults.Users() = %v, want %v", tt.name, got, tt.want)
}
}
}
func Test_showResults_Permissions(t *testing.T) {
t.Parallel()
tests := []struct {
name string
octets []byte
want chronograf.Permissions
}{
{
name: "write for one db",
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","WRITE"]]}]}]`),
want: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"WRITE"},
},
},
},
{
name: "all for one db",
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","ALL PRIVILEGES"]]}]}]`),
want: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"WRITE", "READ"},
},
},
},
{
name: "read for one db",
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","READ"]]}]}]`),
want: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"READ"},
},
},
},
{
name: "other all for one db",
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","ALL"]]}]}]`),
want: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "tensorflowdb",
Allowed: []string{"WRITE", "READ"},
},
},
},
{
name: "other all for one db",
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","NO PRIVILEGES"]]}]}]`),
want: chronograf.Permissions{},
},
{
name: "bad JSON",
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[[1,"WRITE"]]}]}]`),
want: chronograf.Permissions{},
},
{
name: "bad JSON",
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb",1]]}]}]`),
want: chronograf.Permissions{},
},
}
for _, tt := range tests {
r := &showResults{}
json.Unmarshal(tt.octets, r)
if got := r.Permissions(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. showResults.Users() = %v, want %v", tt.name, got, tt.want)
}
}
}

212
influx/users.go Normal file
View File

@ -0,0 +1,212 @@
package influx
import (
"context"
"encoding/json"
"fmt"
"github.com/influxdata/chronograf"
)
// Add a new User in InfluxDB
func (c *Client) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
_, err := c.Query(ctx, chronograf.Query{
Command: fmt.Sprintf(`CREATE USER "%s" WITH PASSWORD '%s'`, u.Name, u.Passwd),
})
if err != nil {
return nil, err
}
return u, nil
}
// Delete the User from InfluxDB
func (c *Client) Delete(ctx context.Context, u *chronograf.User) error {
res, err := c.Query(ctx, chronograf.Query{
Command: fmt.Sprintf(`DROP USER "%s"`, u.Name),
})
if err != nil {
return err
}
// The DROP USER statement puts the error within the results itself
// So, we have to crack open the results to see what happens
octets, err := res.MarshalJSON()
if err != nil {
return err
}
results := make([]struct{ Error string }, 0)
if err := json.Unmarshal(octets, &results); err != nil {
return err
}
// At last, we can check if there are any error strings
for _, r := range results {
if r.Error != "" {
return fmt.Errorf(r.Error)
}
}
return nil
}
// Get retrieves a user if name exists.
func (c *Client) Get(ctx context.Context, name string) (*chronograf.User, error) {
users, err := c.showUsers(ctx)
if err != nil {
return nil, err
}
for _, user := range users {
if user.Name == name {
perms, err := c.userPermissions(ctx, user.Name)
if err != nil {
return nil, err
}
user.Permissions = append(user.Permissions, perms...)
return &user, nil
}
}
return nil, fmt.Errorf("user not found")
}
// Update the user's permissions or roles
func (c *Client) Update(ctx context.Context, u *chronograf.User) error {
// Only allow one type of change at a time. If it is a password
// change then do it and return without any changes to permissions
if u.Passwd != "" {
return c.updatePassword(ctx, u.Name, u.Passwd)
}
user, err := c.Get(ctx, u.Name)
if err != nil {
return err
}
revoke, add := Difference(u.Permissions, user.Permissions)
for _, a := range add {
if err := c.grantPermission(ctx, u.Name, a); err != nil {
return err
}
}
for _, r := range revoke {
if err := c.revokePermission(ctx, u.Name, r); err != nil {
return err
}
}
return nil
}
// All users in influx
func (c *Client) All(ctx context.Context) ([]chronograf.User, error) {
users, err := c.showUsers(ctx)
if err != nil {
return nil, err
}
// For all users we need to look up permissions to add to the user.
for i, user := range users {
perms, err := c.userPermissions(ctx, user.Name)
if err != nil {
return nil, err
}
user.Permissions = append(user.Permissions, perms...)
users[i] = user
}
return users, nil
}
// showUsers runs SHOW USERS InfluxQL command and returns chronograf users.
func (c *Client) showUsers(ctx context.Context) ([]chronograf.User, error) {
res, err := c.Query(ctx, chronograf.Query{
Command: `SHOW USERS`,
})
if err != nil {
return nil, err
}
octets, err := res.MarshalJSON()
if err != nil {
return nil, err
}
results := showResults{}
if err := json.Unmarshal(octets, &results); err != nil {
return nil, err
}
return results.Users(), nil
}
func (c *Client) grantPermission(ctx context.Context, username string, perm chronograf.Permission) error {
query := ToGrant(username, perm)
if query == "" {
return nil
}
_, err := c.Query(ctx, chronograf.Query{
Command: query,
})
return err
}
func (c *Client) revokePermission(ctx context.Context, username string, perm chronograf.Permission) error {
query := ToRevoke(username, perm)
if query == "" {
return nil
}
_, err := c.Query(ctx, chronograf.Query{
Command: query,
})
return err
}
func (c *Client) userPermissions(ctx context.Context, name string) (chronograf.Permissions, error) {
res, err := c.Query(ctx, chronograf.Query{
Command: fmt.Sprintf(`SHOW GRANTS FOR "%s"`, name),
})
if err != nil {
return nil, err
}
octets, err := res.MarshalJSON()
if err != nil {
return nil, err
}
results := showResults{}
if err := json.Unmarshal(octets, &results); err != nil {
return nil, err
}
return results.Permissions(), nil
}
func (c *Client) updatePassword(ctx context.Context, name, passwd string) error {
res, err := c.Query(ctx, chronograf.Query{
Command: fmt.Sprintf(`SET PASSWORD for "%s" = '%s'`, name, passwd),
})
if err != nil {
return err
}
// The SET PASSWORD statements puts the error within the results itself
// So, we have to crack open the results to see what happens
octets, err := res.MarshalJSON()
if err != nil {
return err
}
results := make([]struct{ Error string }, 0)
if err := json.Unmarshal(octets, &results); err != nil {
return err
}
// At last, we can check if there are any error strings
for _, r := range results {
if r.Error != "" {
return fmt.Errorf(r.Error)
}
}
return nil
}

949
influx/users_test.go Normal file
View File

@ -0,0 +1,949 @@
package influx
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/log"
)
func TestClient_userPermissions(t *testing.T) {
t.Parallel()
type args struct {
ctx context.Context
name string
}
tests := []struct {
name string
showGrants []byte
status int
args args
want chronograf.Permissions
wantErr bool
}{
{
name: "Check all grants",
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
status: http.StatusOK,
args: args{
ctx: context.Background(),
name: "docbrown",
},
want: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
{
name: "Permission Denied",
status: http.StatusUnauthorized,
args: args{
ctx: context.Background(),
name: "docbrown",
},
wantErr: true,
},
{
name: "bad JSON",
showGrants: []byte(`{"results":[{"series":"adffdadf"}]}`),
status: http.StatusOK,
args: args{
ctx: context.Background(),
name: "docbrown",
},
wantErr: true,
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
rw.WriteHeader(tt.status)
rw.Write(tt.showGrants)
}))
u, _ := url.Parse(ts.URL)
c := &Client{
URL: u,
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
got, err := c.userPermissions(tt.args.ctx, tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.userPermissions() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Client.userPermissions() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestClient_Add(t *testing.T) {
t.Parallel()
type args struct {
ctx context.Context
u *chronograf.User
}
tests := []struct {
name string
args args
status int
want *chronograf.User
wantQuery string
wantErr bool
}{
{
name: "Create User",
status: http.StatusOK,
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
Passwd: "Dont Need Roads",
},
},
wantQuery: `CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`,
want: &chronograf.User{
Name: "docbrown",
Passwd: "Dont Need Roads",
},
},
{
name: "Permission Denied",
status: http.StatusUnauthorized,
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
Passwd: "Dont Need Roads",
},
},
wantQuery: `CREATE USER "docbrown" WITH PASSWORD 'Dont Need Roads'`,
wantErr: true,
},
}
for _, tt := range tests {
query := ""
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
query = r.URL.Query().Get("q")
rw.WriteHeader(tt.status)
rw.Write([]byte(`{"results":[{}]}`))
}))
u, _ := url.Parse(ts.URL)
c := &Client{
URL: u,
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
got, err := c.Add(tt.args.ctx, tt.args.u)
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Add() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if tt.wantQuery != query {
t.Errorf("%q. Client.Add() query = %v, want %v", tt.name, query, tt.wantQuery)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Client.Add() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestClient_Delete(t *testing.T) {
type args struct {
ctx context.Context
u *chronograf.User
}
tests := []struct {
name string
status int
dropUser []byte
args args
wantErr bool
}{
{
name: "Drop User",
dropUser: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
status: http.StatusOK,
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
},
},
},
{
name: "No such user",
dropUser: []byte(`{"results":[{"error":"user not found"}]}`),
status: http.StatusOK,
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
},
},
wantErr: true,
},
{
name: "Bad InfluxQL",
dropUser: []byte(`{"error":"error parsing query: found doody, expected ; at line 1, char 17"}`),
status: http.StatusBadRequest,
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
},
},
wantErr: true,
},
{
name: "Bad JSON",
dropUser: []byte(`{"results":[{"error":breakhere}]}`),
status: http.StatusOK,
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
},
},
wantErr: true,
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
rw.WriteHeader(tt.status)
rw.Write(tt.dropUser)
}))
u, _ := url.Parse(ts.URL)
c := &Client{
URL: u,
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
if err := c.Delete(tt.args.ctx, tt.args.u); (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Delete() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
}
}
func TestClient_Get(t *testing.T) {
type args struct {
ctx context.Context
name string
}
tests := []struct {
name string
args args
statusUsers int
showUsers []byte
statusGrants int
showGrants []byte
want *chronograf.User
wantErr bool
}{
{
name: "Get User",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
args: args{
ctx: context.Background(),
name: "docbrown",
},
want: &chronograf.User{
Name: "docbrown",
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "all",
Allowed: []string{"WRITE", "READ"},
},
chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
},
{
name: "Fail show users",
statusUsers: http.StatusBadRequest,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
args: args{
ctx: context.Background(),
name: "docbrown",
},
wantErr: true,
},
{
name: "Fail show grants",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusBadRequest,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
args: args{
ctx: context.Background(),
name: "docbrown",
},
wantErr: true,
},
{
name: "Fail no such user",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true]]}]}]}`),
args: args{
ctx: context.Background(),
name: "docbrown",
},
wantErr: true,
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
query := r.URL.Query().Get("q")
if strings.Contains(query, "GRANTS") {
rw.WriteHeader(tt.statusGrants)
rw.Write(tt.showGrants)
} else if strings.Contains(query, "USERS") {
rw.WriteHeader(tt.statusUsers)
rw.Write(tt.showUsers)
}
}))
u, _ := url.Parse(ts.URL)
c := &Client{
URL: u,
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
got, err := c.Get(tt.args.ctx, tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Get() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Client.Get() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestClient_grantPermission(t *testing.T) {
type args struct {
ctx context.Context
username string
perm chronograf.Permission
}
tests := []struct {
name string
args args
status int
results []byte
wantQuery string
wantErr bool
}{
{
name: "simple grants",
status: http.StatusOK,
results: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
username: "docbrown",
perm: chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
wantQuery: `GRANT ALL ON "mydb" TO "docbrown"`,
},
{
name: "bad grants",
status: http.StatusOK,
results: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
username: "docbrown",
perm: chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"howdy"},
},
},
wantQuery: ``,
},
{
name: "no grants",
status: http.StatusOK,
results: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
username: "docbrown",
perm: chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{},
},
},
wantQuery: ``,
},
}
for _, tt := range tests {
query := ""
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
query = r.URL.Query().Get("q")
rw.WriteHeader(tt.status)
rw.Write(tt.results)
}))
u, _ := url.Parse(ts.URL)
c := &Client{
URL: u,
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
if err := c.grantPermission(tt.args.ctx, tt.args.username, tt.args.perm); (err != nil) != tt.wantErr {
t.Errorf("%q. Client.grantPermission() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
if query != tt.wantQuery {
t.Errorf("%q. Client.grantPermission() = %v, want %v", tt.name, query, tt.wantQuery)
}
}
}
func TestClient_revokePermission(t *testing.T) {
type args struct {
ctx context.Context
username string
perm chronograf.Permission
}
tests := []struct {
name string
args args
status int
results []byte
wantQuery string
wantErr bool
}{
{
name: "simple revoke",
status: http.StatusOK,
results: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
username: "docbrown",
perm: chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
wantQuery: `REVOKE ALL ON "mydb" FROM "docbrown"`,
},
{
name: "bad revoke",
status: http.StatusOK,
results: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
username: "docbrown",
perm: chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"howdy"},
},
},
wantQuery: ``,
},
{
name: "no permissions",
status: http.StatusOK,
results: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
username: "docbrown",
perm: chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{},
},
},
wantQuery: `REVOKE ALL PRIVILEGES ON "mydb" FROM "docbrown"`,
},
}
for _, tt := range tests {
query := ""
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
query = r.URL.Query().Get("q")
rw.WriteHeader(tt.status)
rw.Write(tt.results)
}))
u, _ := url.Parse(ts.URL)
c := &Client{
URL: u,
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
if err := c.revokePermission(tt.args.ctx, tt.args.username, tt.args.perm); (err != nil) != tt.wantErr {
t.Errorf("%q. Client.revokePermission() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
if query != tt.wantQuery {
t.Errorf("%q. Client.revokePermission() = %v, want %v", tt.name, query, tt.wantQuery)
}
}
}
func TestClient_All(t *testing.T) {
type args struct {
ctx context.Context
}
tests := []struct {
name string
args args
statusUsers int
showUsers []byte
statusGrants int
showGrants []byte
want []chronograf.User
wantErr bool
}{
{
name: "All Users",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
args: args{
ctx: context.Background(),
},
want: []chronograf.User{
{
Name: "admin",
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "all",
Allowed: []string{"WRITE", "READ"},
},
chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
{
Name: "docbrown",
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "all",
Allowed: []string{"WRITE", "READ"},
},
chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
{
Name: "reader",
Permissions: chronograf.Permissions{
chronograf.Permission{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
},
},
{
name: "Unauthorized",
statusUsers: http.StatusUnauthorized,
showUsers: []byte(`{}`),
args: args{
ctx: context.Background(),
},
wantErr: true,
},
{
name: "Permission error",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusBadRequest,
showGrants: []byte(`{}`),
args: args{
ctx: context.Background(),
},
wantErr: true,
},
}
for _, tt := range tests {
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
query := r.URL.Query().Get("q")
if strings.Contains(query, "GRANTS") {
rw.WriteHeader(tt.statusGrants)
rw.Write(tt.showGrants)
} else if strings.Contains(query, "USERS") {
rw.WriteHeader(tt.statusUsers)
rw.Write(tt.showUsers)
}
}))
u, _ := url.Parse(ts.URL)
c := &Client{
URL: u,
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
got, err := c.All(tt.args.ctx)
if (err != nil) != tt.wantErr {
t.Errorf("%q. Client.All() error = %v, wantErr %v", tt.name, err, tt.wantErr)
continue
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("%q. Client.All() = %v, want %v", tt.name, got, tt.want)
}
}
}
func TestClient_Update(t *testing.T) {
type args struct {
ctx context.Context
u *chronograf.User
}
tests := []struct {
name string
statusUsers int
showUsers []byte
statusGrants int
showGrants []byte
statusRevoke int
revoke []byte
statusGrant int
grant []byte
statusPassword int
password []byte
args args
want []string
wantErr bool
}{
{
name: "Change Password",
statusPassword: http.StatusOK,
password: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
Passwd: "hunter2",
},
},
want: []string{
`SET PASSWORD for "docbrown" = 'hunter2'`,
},
},
{
name: "Grant all permissions",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
statusRevoke: http.StatusOK,
revoke: []byte(`{"results":[]}`),
statusGrant: http.StatusOK,
grant: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
Permissions: chronograf.Permissions{
{
Scope: "all",
Allowed: []string{"WRITE", "READ"},
},
{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
},
want: []string{
`SHOW USERS`,
`SHOW GRANTS FOR "docbrown"`,
`GRANT ALL PRIVILEGES TO "docbrown"`,
`GRANT ALL ON "mydb" TO "docbrown"`,
},
},
{
name: "Revoke all permissions",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
statusRevoke: http.StatusOK,
revoke: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
},
},
want: []string{
`SHOW USERS`,
`SHOW GRANTS FOR "docbrown"`,
`REVOKE ALL PRIVILEGES FROM "docbrown"`,
`REVOKE ALL ON "mydb" FROM "docbrown"`,
},
},
{
name: "Grant all permissions",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
statusRevoke: http.StatusOK,
revoke: []byte(`{"results":[]}`),
statusGrant: http.StatusOK,
grant: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
Permissions: chronograf.Permissions{
{
Scope: "all",
Allowed: []string{"WRITE", "READ"},
},
{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE", "READ"},
},
},
},
},
want: []string{
`SHOW USERS`,
`SHOW GRANTS FOR "docbrown"`,
`GRANT ALL PRIVILEGES TO "docbrown"`,
`GRANT ALL ON "mydb" TO "docbrown"`,
},
},
{
name: "Revoke some add some",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
statusRevoke: http.StatusOK,
revoke: []byte(`{"results":[]}`),
statusGrant: http.StatusOK,
grant: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
Permissions: chronograf.Permissions{
{
Scope: "all",
Allowed: []string{},
},
{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE"},
},
{
Scope: "database",
Name: "newdb",
Allowed: []string{"WRITE", "READ"},
},
},
},
},
want: []string{
`SHOW USERS`,
`SHOW GRANTS FOR "docbrown"`,
`GRANT WRITE ON "mydb" TO "docbrown"`,
`GRANT ALL ON "newdb" TO "docbrown"`,
`REVOKE ALL PRIVILEGES FROM "docbrown"`,
},
},
{
name: "Fail users",
statusUsers: http.StatusBadRequest,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
statusRevoke: http.StatusOK,
revoke: []byte(`{"results":[]}`),
statusGrant: http.StatusOK,
grant: []byte(`{"results":[]}`),
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
},
},
wantErr: true,
want: []string{
`SHOW USERS`,
},
},
{
name: "fail grants",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
statusRevoke: http.StatusOK,
revoke: []byte(`{"results":[]}`),
statusGrant: http.StatusBadRequest,
grant: []byte(`{"results":[]}`),
wantErr: true,
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
Permissions: chronograf.Permissions{
{
Scope: "all",
Allowed: []string{},
},
{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE"},
},
{
Scope: "database",
Name: "newdb",
Allowed: []string{"WRITE", "READ"},
},
},
},
},
want: []string{
`SHOW USERS`,
`SHOW GRANTS FOR "docbrown"`,
`GRANT WRITE ON "mydb" TO "docbrown"`,
},
},
{
name: "fail revoke",
statusUsers: http.StatusOK,
showUsers: []byte(`{"results":[{"series":[{"columns":["user","admin"],"values":[["admin",true],["docbrown",true],["reader",false]]}]}]}`),
statusGrants: http.StatusOK,
showGrants: []byte(`{"results":[{"series":[{"columns":["database","privilege"],"values":[["mydb","ALL PRIVILEGES"]]}]}]}`),
statusRevoke: http.StatusBadRequest,
revoke: []byte(`{"results":[]}`),
statusGrant: http.StatusOK,
grant: []byte(`{"results":[]}`),
wantErr: true,
args: args{
ctx: context.Background(),
u: &chronograf.User{
Name: "docbrown",
Permissions: chronograf.Permissions{
{
Scope: "all",
Allowed: []string{},
},
{
Scope: "database",
Name: "mydb",
Allowed: []string{"WRITE"},
},
{
Scope: "database",
Name: "newdb",
Allowed: []string{"WRITE", "READ"},
},
},
},
},
want: []string{
`SHOW USERS`,
`SHOW GRANTS FOR "docbrown"`,
`GRANT WRITE ON "mydb" TO "docbrown"`,
`GRANT ALL ON "newdb" TO "docbrown"`,
`REVOKE ALL PRIVILEGES FROM "docbrown"`,
},
},
}
for _, tt := range tests {
queries := []string{}
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was", path)
}
query := r.URL.Query().Get("q")
if strings.Contains(query, "GRANTS") {
rw.WriteHeader(tt.statusGrants)
rw.Write(tt.showGrants)
} else if strings.Contains(query, "USERS") {
rw.WriteHeader(tt.statusUsers)
rw.Write(tt.showUsers)
} else if strings.Contains(query, "REVOKE") {
rw.WriteHeader(tt.statusRevoke)
rw.Write(tt.revoke)
} else if strings.Contains(query, "GRANT") {
rw.WriteHeader(tt.statusGrant)
rw.Write(tt.grant)
} else if strings.Contains(query, "PASSWORD") {
rw.WriteHeader(tt.statusPassword)
rw.Write(tt.password)
}
queries = append(queries, query)
}))
u, _ := url.Parse(ts.URL)
c := &Client{
URL: u,
Logger: log.New(log.DebugLevel),
}
defer ts.Close()
if err := c.Update(tt.args.ctx, tt.args.u); (err != nil) != tt.wantErr {
t.Errorf("%q. Client.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
if !reflect.DeepEqual(queries, tt.want) {
t.Errorf("%q. Client.Update() = %v, want %v", tt.name, queries, tt.want)
}
}
}
/*
*/

43
mocks/roles.go Normal file
View File

@ -0,0 +1,43 @@
package mocks
import (
"context"
"github.com/influxdata/chronograf"
)
var _ chronograf.RolesStore = &RolesStore{}
// RolesStore mock allows all functions to be set for testing
type RolesStore struct {
AllF func(context.Context) ([]chronograf.Role, error)
AddF func(context.Context, *chronograf.Role) (*chronograf.Role, error)
DeleteF func(context.Context, *chronograf.Role) error
GetF func(ctx context.Context, name string) (*chronograf.Role, error)
UpdateF func(context.Context, *chronograf.Role) error
}
// All lists all Roles from the RolesStore
func (s *RolesStore) All(ctx context.Context) ([]chronograf.Role, error) {
return s.AllF(ctx)
}
// Add a new Role in the RolesStore
func (s *RolesStore) Add(ctx context.Context, u *chronograf.Role) (*chronograf.Role, error) {
return s.AddF(ctx, u)
}
// Delete the Role from the RolesStore
func (s *RolesStore) Delete(ctx context.Context, u *chronograf.Role) error {
return s.DeleteF(ctx, u)
}
// Get retrieves a Role if name exists.
func (s *RolesStore) Get(ctx context.Context, name string) (*chronograf.Role, error) {
return s.GetF(ctx, name)
}
// Update the Role's permissions or users
func (s *RolesStore) Update(ctx context.Context, u *chronograf.Role) error {
return s.UpdateF(ctx, u)
}

43
mocks/sources.go Normal file
View File

@ -0,0 +1,43 @@
package mocks
import (
"context"
"github.com/influxdata/chronograf"
)
var _ chronograf.SourcesStore = &SourcesStore{}
// SourcesStore mock allows all functions to be set for testing
type SourcesStore struct {
AllF func(context.Context) ([]chronograf.Source, error)
AddF func(context.Context, chronograf.Source) (chronograf.Source, error)
DeleteF func(context.Context, chronograf.Source) error
GetF func(ctx context.Context, ID int) (chronograf.Source, error)
UpdateF func(context.Context, chronograf.Source) error
}
// All returns all sources in the store
func (s *SourcesStore) All(ctx context.Context) ([]chronograf.Source, error) {
return s.AllF(ctx)
}
// Add creates a new source in the SourcesStore and returns Source with ID
func (s *SourcesStore) Add(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
return s.AddF(ctx, src)
}
// Delete the Source from the store
func (s *SourcesStore) Delete(ctx context.Context, src chronograf.Source) error {
return s.DeleteF(ctx, src)
}
// Get retrieves Source if `ID` exists
func (s *SourcesStore) Get(ctx context.Context, ID int) (chronograf.Source, error) {
return s.GetF(ctx, ID)
}
// Update the Source in the store.
func (s *SourcesStore) Update(ctx context.Context, src chronograf.Source) error {
return s.UpdateF(ctx, src)
}

53
mocks/timeseries.go Normal file
View File

@ -0,0 +1,53 @@
package mocks
import (
"context"
"github.com/influxdata/chronograf"
)
var _ chronograf.TimeSeries = &TimeSeries{}
// TimeSeries is a mockable chronograf time series by overriding the functions.
type TimeSeries struct {
// Query retrieves time series data from the database.
QueryF func(context.Context, chronograf.Query) (chronograf.Response, error)
// Connect will connect to the time series using the information in `Source`.
ConnectF func(context.Context, *chronograf.Source) error
// UsersStore represents the user accounts within the TimeSeries database
UsersF func(context.Context) chronograf.UsersStore
// Allowances returns all valid names permissions in this database
AllowancesF func(context.Context) chronograf.Allowances
// RolesF represents the roles. Roles group permissions and Users
RolesF func(context.Context) (chronograf.RolesStore, error)
}
// New implements TimeSeriesClient
func (t *TimeSeries) New(chronograf.Source, chronograf.Logger) (chronograf.TimeSeries, error) {
return t, nil
}
// Query retrieves time series data from the database.
func (t *TimeSeries) Query(ctx context.Context, query chronograf.Query) (chronograf.Response, error) {
return t.QueryF(ctx, query)
}
// Connect will connect to the time series using the information in `Source`.
func (t *TimeSeries) Connect(ctx context.Context, src *chronograf.Source) error {
return t.ConnectF(ctx, src)
}
// Users represents the user accounts within the TimeSeries database
func (t *TimeSeries) Users(ctx context.Context) chronograf.UsersStore {
return t.UsersF(ctx)
}
// Roles represents the roles. Roles group permissions and Users
func (t *TimeSeries) Roles(ctx context.Context) (chronograf.RolesStore, error) {
return t.RolesF(ctx)
}
// Allowances returns all valid names permissions in this database
func (t *TimeSeries) Allowances(ctx context.Context) chronograf.Allowances {
return t.AllowancesF(ctx)
}

43
mocks/users.go Normal file
View File

@ -0,0 +1,43 @@
package mocks
import (
"context"
"github.com/influxdata/chronograf"
)
var _ chronograf.UsersStore = &UsersStore{}
// UsersStore mock allows all functions to be set for testing
type UsersStore struct {
AllF func(context.Context) ([]chronograf.User, error)
AddF func(context.Context, *chronograf.User) (*chronograf.User, error)
DeleteF func(context.Context, *chronograf.User) error
GetF func(ctx context.Context, name string) (*chronograf.User, error)
UpdateF func(context.Context, *chronograf.User) error
}
// All lists all users from the UsersStore
func (s *UsersStore) All(ctx context.Context) ([]chronograf.User, error) {
return s.AllF(ctx)
}
// Add a new User in the UsersStore
func (s *UsersStore) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return s.AddF(ctx, u)
}
// Delete the User from the UsersStore
func (s *UsersStore) Delete(ctx context.Context, u *chronograf.User) error {
return s.DeleteF(ctx, u)
}
// Get retrieves a user if name exists.
func (s *UsersStore) Get(ctx context.Context, name string) (*chronograf.User, error) {
return s.GetF(ctx, name)
}
// Update the user's permissions or roles
func (s *UsersStore) Update(ctx context.Context, u *chronograf.User) error {
return s.UpdateF(ctx, u)
}

View File

@ -9,18 +9,21 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
/* Constants */ type principalKey string
const (
func (p principalKey) String() string {
return string(p)
}
var (
// PrincipalKey is used to pass principal // PrincipalKey is used to pass principal
// via context.Context to request-scoped // via context.Context to request-scoped
// functions. // functions.
PrincipalKey string = "principal" PrincipalKey = principalKey("principal")
) // ErrAuthentication means that oauth2 exchange failed
var (
/* Errors */
ErrAuthentication = errors.New("user not authenticated") ErrAuthentication = errors.New("user not authenticated")
ErrOrgMembership = errors.New("Not a member of the required organization") // ErrOrgMembership means that the user is not in the OAuth2 filtered group
ErrOrgMembership = errors.New("Not a member of the required organization")
) )
/* Types */ /* Types */

529
server/admin.go Normal file
View File

@ -0,0 +1,529 @@
package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
)
func validPermissions(perms *chronograf.Permissions) error {
if perms == nil {
return nil
}
for _, perm := range *perms {
if perm.Scope != chronograf.AllScope && perm.Scope != chronograf.DBScope {
return fmt.Errorf("Invalid permission scope")
}
if perm.Scope == chronograf.DBScope && perm.Name == "" {
return fmt.Errorf("Database scoped permission requires a name")
}
}
return nil
}
type sourceUserRequest struct {
Username string `json:"name,omitempty"` // Username for new account
Password string `json:"password,omitempty"` // Password for new account
Permissions chronograf.Permissions `json:"permissions,omitempty"` // Optional permissions
}
func (r *sourceUserRequest) ValidCreate() error {
if r.Username == "" {
return fmt.Errorf("Username required")
}
if r.Password == "" {
return fmt.Errorf("Password required")
}
return validPermissions(&r.Permissions)
}
func (r *sourceUserRequest) ValidUpdate() error {
if r.Password == "" && len(r.Permissions) == 0 {
return fmt.Errorf("No fields to update")
}
return validPermissions(&r.Permissions)
}
type sourceUser struct {
Username string `json:"name,omitempty"` // Username for new account
Permissions chronograf.Permissions `json:"permissions,omitempty"` // Account's permissions
Links selfLinks `json:"links"` // Links are URI locations related to user
}
type selfLinks struct {
Self string `json:"self"` // Self link mapping to this resource
}
func newSelfLinks(id int, parent, resource string) selfLinks {
httpAPISrcs := "/chronograf/v1/sources"
u := &url.URL{Path: resource}
encodedResource := u.String()
return selfLinks{
Self: fmt.Sprintf("%s/%d/%s/%s", httpAPISrcs, id, parent, encodedResource),
}
}
// NewSourceUser adds user to source
func (h *Service) NewSourceUser(w http.ResponseWriter, r *http.Request) {
var req sourceUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := req.ValidCreate(); err != nil {
invalidData(w, err, h.Logger)
return
}
ctx := r.Context()
srcID, store, err := h.sourceUsersStore(ctx, w, r)
if err != nil {
return
}
user := &chronograf.User{
Name: req.Username,
Passwd: req.Password,
}
res, err := store.Add(ctx, user)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
su := sourceUser{
Username: res.Name,
Permissions: req.Permissions,
Links: newSelfLinks(srcID, "users", res.Name),
}
w.Header().Add("Location", su.Links.Self)
encodeJSON(w, http.StatusCreated, su, h.Logger)
}
type sourceUsers struct {
Users []sourceUser `json:"users"`
}
// SourceUsers retrieves all users from source.
func (h *Service) SourceUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, store, err := h.sourceUsersStore(ctx, w, r)
if err != nil {
return
}
users, err := store.All(ctx)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
su := []sourceUser{}
for _, u := range users {
su = append(su, sourceUser{
Username: u.Name,
Permissions: u.Permissions,
Links: newSelfLinks(srcID, "users", u.Name),
})
}
res := sourceUsers{
Users: su,
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// SourceUserID retrieves a user with ID from store.
func (h *Service) SourceUserID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid := httprouter.GetParamFromContext(ctx, "uid")
srcID, store, err := h.sourceUsersStore(ctx, w, r)
if err != nil {
return
}
u, err := store.Get(ctx, uid)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
res := sourceUser{
Username: u.Name,
Permissions: u.Permissions,
Links: newSelfLinks(srcID, "users", u.Name),
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// RemoveSourceUser removes the user from the InfluxDB source
func (h *Service) RemoveSourceUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid := httprouter.GetParamFromContext(ctx, "uid")
_, store, err := h.sourceUsersStore(ctx, w, r)
if err != nil {
return
}
if err := store.Delete(ctx, &chronograf.User{Name: uid}); err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
// UpdateSourceUser changes the password or permissions of a source user
func (h *Service) UpdateSourceUser(w http.ResponseWriter, r *http.Request) {
var req sourceUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := req.ValidUpdate(); err != nil {
invalidData(w, err, h.Logger)
return
}
ctx := r.Context()
uid := httprouter.GetParamFromContext(ctx, "uid")
srcID, store, err := h.sourceUsersStore(ctx, w, r)
if err != nil {
return
}
user := &chronograf.User{
Name: uid,
Passwd: req.Password,
Permissions: req.Permissions,
}
if err := store.Update(ctx, user); err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
su := sourceUser{
Username: user.Name,
Permissions: user.Permissions,
Links: newSelfLinks(srcID, "users", user.Name),
}
w.Header().Add("Location", su.Links.Self)
encodeJSON(w, http.StatusOK, su, h.Logger)
}
func (h *Service) sourcesSeries(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.TimeSeries, error) {
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return 0, nil, err
}
src, err := h.SourcesStore.Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
return 0, nil, err
}
ts, err := h.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
return 0, nil, err
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
return 0, nil, err
}
return srcID, ts, nil
}
func (h *Service) sourceUsersStore(ctx context.Context, w http.ResponseWriter, r *http.Request) (int, chronograf.UsersStore, error) {
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return 0, nil, err
}
store := ts.Users(ctx)
return srcID, store, nil
}
// hasRoles checks if the influx source has roles or not
func (h *Service) hasRoles(ctx context.Context, ts chronograf.TimeSeries) (chronograf.RolesStore, bool) {
store, err := ts.Roles(ctx)
if err != nil {
return nil, false
}
return store, true
}
// Permissions returns all possible permissions for this source.
func (h *Service) Permissions(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
src, err := h.SourcesStore.Get(ctx, srcID)
if err != nil {
notFound(w, srcID, h.Logger)
return
}
ts, err := h.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
return
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", srcID, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
return
}
perms := ts.Allowances(ctx)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
httpAPISrcs := "/chronograf/v1/sources"
res := struct {
Permissions chronograf.Allowances `json:"permissions"`
Links map[string]string `json:"links"` // Links are URI locations related to user
}{
Permissions: perms,
Links: map[string]string{
"self": fmt.Sprintf("%s/%d/permissions", httpAPISrcs, srcID),
"source": fmt.Sprintf("%s/%d", httpAPISrcs, srcID),
},
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
type sourceRoleRequest struct {
chronograf.Role
}
func (r *sourceRoleRequest) ValidCreate() error {
if r.Name == "" || len(r.Name) > 254 {
return fmt.Errorf("Name is required for a role")
}
for _, user := range r.Users {
if user.Name == "" {
return fmt.Errorf("Username required")
}
}
return validPermissions(&r.Permissions)
}
func (r *sourceRoleRequest) ValidUpdate() error {
if len(r.Name) > 254 {
return fmt.Errorf("Username too long; must be less than 254 characters")
}
for _, user := range r.Users {
if user.Name == "" {
return fmt.Errorf("Username required")
}
}
return validPermissions(&r.Permissions)
}
type roleResponse struct {
Users []sourceUser `json:"users"`
Name string `json:"name"`
Permissions chronograf.Permissions `json:"permissions"`
Links selfLinks `json:"links"`
}
func newRoleResponse(srcID int, res *chronograf.Role) roleResponse {
su := make([]sourceUser, len(res.Users))
for i := range res.Users {
name := res.Users[i].Name
su[i] = sourceUser{
Username: name,
Links: newSelfLinks(srcID, "users", name),
}
}
if res.Permissions == nil {
res.Permissions = make(chronograf.Permissions, 0)
}
return roleResponse{
Name: res.Name,
Permissions: res.Permissions,
Users: su,
Links: newSelfLinks(srcID, "roles", res.Name),
}
}
// NewRole adds role to source
func (h *Service) NewRole(w http.ResponseWriter, r *http.Request) {
var req sourceRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := req.ValidCreate(); err != nil {
invalidData(w, err, h.Logger)
return
}
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
res, err := roles.Add(ctx, &req.Role)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
rr := newRoleResponse(srcID, res)
w.Header().Add("Location", rr.Links.Self)
encodeJSON(w, http.StatusCreated, rr, h.Logger)
}
// UpdateRole changes the permissions or users of a role
func (h *Service) UpdateRole(w http.ResponseWriter, r *http.Request) {
var req sourceRoleRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := req.ValidUpdate(); err != nil {
invalidData(w, err, h.Logger)
return
}
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
rid := httprouter.GetParamFromContext(ctx, "rid")
req.Name = rid
if err := roles.Update(ctx, &req.Role); err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
role, err := roles.Get(ctx, req.Name)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
rr := newRoleResponse(srcID, role)
w.Header().Add("Location", rr.Links.Self)
encodeJSON(w, http.StatusOK, rr, h.Logger)
}
// RoleID retrieves a role with ID from store.
func (h *Service) RoleID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
rid := httprouter.GetParamFromContext(ctx, "rid")
role, err := roles.Get(ctx, rid)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
rr := newRoleResponse(srcID, role)
encodeJSON(w, http.StatusOK, rr, h.Logger)
}
// Roles retrieves all roles from the store
func (h *Service) Roles(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
store, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
roles, err := store.All(ctx)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
rr := make([]roleResponse, len(roles))
for i, role := range roles {
rr[i] = newRoleResponse(srcID, &role)
}
res := struct {
Roles []roleResponse `json:"roles"`
}{rr}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// RemoveRole removes role from data source.
func (h *Service) RemoveRole(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
srcID, ts, err := h.sourcesSeries(ctx, w, r)
if err != nil {
return
}
roles, ok := h.hasRoles(ctx, ts)
if !ok {
Error(w, http.StatusNotFound, fmt.Sprintf("Source %d does not have role capability", srcID), h.Logger)
return
}
rid := httprouter.GetParamFromContext(ctx, "rid")
if err := roles.Delete(ctx, &chronograf.Role{Name: rid}); err != nil {
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}

1471
server/admin_test.go Normal file

File diff suppressed because it is too large Load Diff

77
server/influx.go Normal file
View File

@ -0,0 +1,77 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"github.com/influxdata/chronograf"
)
// ValidInfluxRequest checks if queries specify a command.
func ValidInfluxRequest(p chronograf.Query) error {
if p.Command == "" {
return fmt.Errorf("query field required")
}
return nil
}
type postInfluxResponse struct {
Results interface{} `json:"results"` // results from influx
}
// Influx proxies requests to infludb.
func (h *Service) Influx(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
var req chronograf.Query
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err = ValidInfluxRequest(req); err != nil {
invalidData(w, err, h.Logger)
return
}
ctx := r.Context()
src, err := h.SourcesStore.Get(ctx, id)
if err != nil {
notFound(w, id, h.Logger)
return
}
ts, err := h.TimeSeries(src)
if err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
return
}
if err = ts.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d: %v", id, err)
Error(w, http.StatusBadRequest, msg, h.Logger)
return
}
response, err := ts.Query(ctx, req)
if err != nil {
if err == chronograf.ErrUpstreamTimeout {
msg := "Timeout waiting for Influx response"
Error(w, http.StatusRequestTimeout, msg, h.Logger)
return
}
// TODO: Here I want to return the error code from influx.
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
res := postInfluxResponse{
Results: response,
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}

View File

@ -445,10 +445,12 @@ func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
encodeJSON(w, http.StatusOK, res, h.Logger) encodeJSON(w, http.StatusOK, res, h.Logger)
} }
// KapacitorStatus is the current state of a running task
type KapacitorStatus struct { type KapacitorStatus struct {
Status string `json:"status"` Status string `json:"status"`
} }
// Valid check if the kapacitor status is enabled or disabled
func (k *KapacitorStatus) Valid() error { func (k *KapacitorStatus) Valid() error {
if k.Status == "enabled" || k.Status == "disabled" { if k.Status == "enabled" || k.Status == "disabled" {
return nil return nil

View File

@ -63,8 +63,27 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.PATCH("/chronograf/v1/sources/:id", service.UpdateSource) router.PATCH("/chronograf/v1/sources/:id", service.UpdateSource)
router.DELETE("/chronograf/v1/sources/:id", service.RemoveSource) router.DELETE("/chronograf/v1/sources/:id", service.RemoveSource)
// Source Proxy // Source Proxy to Influx
router.POST("/chronograf/v1/sources/:id/proxy", service.Proxy) router.POST("/chronograf/v1/sources/:id/proxy", service.Influx)
// All possible permissions for users in this source
router.GET("/chronograf/v1/sources/:id/permissions", service.Permissions)
// Users associated with the data source
router.GET("/chronograf/v1/sources/:id/users", service.SourceUsers)
router.POST("/chronograf/v1/sources/:id/users", service.NewSourceUser)
router.GET("/chronograf/v1/sources/:id/users/:uid", service.SourceUserID)
router.DELETE("/chronograf/v1/sources/:id/users/:uid", service.RemoveSourceUser)
router.PATCH("/chronograf/v1/sources/:id/users/:uid", service.UpdateSourceUser)
// Roles associated with the data source
router.GET("/chronograf/v1/sources/:id/roles", service.Roles)
router.POST("/chronograf/v1/sources/:id/roles", service.NewRole)
router.GET("/chronograf/v1/sources/:id/roles/:rid", service.RoleID)
router.DELETE("/chronograf/v1/sources/:id/roles/:rid", service.RemoveRole)
router.PATCH("/chronograf/v1/sources/:id/roles/:rid", service.UpdateRole)
// Kapacitor // Kapacitor
router.GET("/chronograf/v1/sources/:id/kapacitors", service.Kapacitors) router.GET("/chronograf/v1/sources/:id/kapacitors", service.Kapacitors)
@ -102,11 +121,6 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
// Users // Users
router.GET("/chronograf/v1/me", service.Me) router.GET("/chronograf/v1/me", service.Me)
router.POST("/chronograf/v1/users", service.NewUser)
router.GET("/chronograf/v1/users/:id", service.UserID)
router.PATCH("/chronograf/v1/users/:id", service.UpdateUser)
router.DELETE("/chronograf/v1/users/:id", service.RemoveUser)
// Dashboards // Dashboards
router.GET("/chronograf/v1/dashboards", service.Dashboards) router.GET("/chronograf/v1/dashboards", service.Dashboards)

View File

@ -2,76 +2,12 @@ package server
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"github.com/influxdata/chronograf"
) )
// ValidProxyRequest checks if queries specify a command.
func ValidProxyRequest(p chronograf.Query) error {
if p.Command == "" {
return fmt.Errorf("query field required")
}
return nil
}
type postProxyResponse struct {
Results interface{} `json:"results"` // results from influx
}
// Proxy proxies requests to infludb.
func (h *Service) Proxy(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
var req chronograf.Query
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err = ValidProxyRequest(req); err != nil {
invalidData(w, err, h.Logger)
return
}
ctx := r.Context()
src, err := h.SourcesStore.Get(ctx, id)
if err != nil {
notFound(w, id, h.Logger)
return
}
if err = h.TimeSeries.Connect(ctx, &src); err != nil {
msg := fmt.Sprintf("Unable to connect to source %d", id)
Error(w, http.StatusBadRequest, msg, h.Logger)
return
}
response, err := h.TimeSeries.Query(ctx, req)
if err != nil {
if err == chronograf.ErrUpstreamTimeout {
msg := "Timeout waiting for Influx response"
Error(w, http.StatusRequestTimeout, msg, h.Logger)
return
}
// TODO: Here I want to return the error code from influx.
Error(w, http.StatusBadRequest, err.Error(), h.Logger)
return
}
res := postProxyResponse{
Results: response,
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// KapacitorProxy proxies requests to kapacitor using the path query parameter. // KapacitorProxy proxies requests to kapacitor using the path query parameter.
func (h *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) { func (h *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) {
srcID, err := paramID("id", r) srcID, err := paramID("id", r)

View File

@ -32,7 +32,6 @@ type getRoutesResponse struct {
Layouts string `json:"layouts"` // Location of the layouts endpoint Layouts string `json:"layouts"` // Location of the layouts endpoint
Mappings string `json:"mappings"` // Location of the application mappings endpoint Mappings string `json:"mappings"` // Location of the application mappings endpoint
Sources string `json:"sources"` // Location of the sources endpoint Sources string `json:"sources"` // Location of the sources endpoint
Users string `json:"users"` // Location of the users endpoint
Me string `json:"me"` // Location of the me endpoint Me string `json:"me"` // Location of the me endpoint
Dashboards string `json:"dashboards"` // Location of the dashboards endpoint Dashboards string `json:"dashboards"` // Location of the dashboards endpoint
Auth []AuthRoute `json:"auth"` // Location of all auth routes. Auth []AuthRoute `json:"auth"` // Location of all auth routes.
@ -43,7 +42,6 @@ func AllRoutes(authRoutes []AuthRoute, logger chronograf.Logger) http.HandlerFun
routes := getRoutesResponse{ routes := getRoutesResponse{
Sources: "/chronograf/v1/sources", Sources: "/chronograf/v1/sources",
Layouts: "/chronograf/v1/layouts", Layouts: "/chronograf/v1/layouts",
Users: "/chronograf/v1/users",
Me: "/chronograf/v1/me", Me: "/chronograf/v1/me",
Mappings: "/chronograf/v1/mappings", Mappings: "/chronograf/v1/mappings",
Dashboards: "/chronograf/v1/dashboards", Dashboards: "/chronograf/v1/dashboards",
@ -59,33 +57,3 @@ func AllRoutes(authRoutes []AuthRoute, logger chronograf.Logger) http.HandlerFun
return return
}) })
} }
func NewGithubRoute() AuthRoute {
return AuthRoute{
Name: "github",
Label: "GitHub",
Login: "/oauth/github/login",
Logout: "/oauth/github/logout",
Callback: "/oauth/github/callback",
}
}
func NewGoogleRoute() AuthRoute {
return AuthRoute{
Name: "google",
Label: "Google",
Login: "/oauth/google/login",
Logout: "/oauth/google/logout",
Callback: "/oauth/google/callback",
}
}
func NewHerokuRoute() AuthRoute {
return AuthRoute{
Name: "heroku",
Label: "Heroku",
Login: "/oauth/heroku/login",
Logout: "/oauth/heroku/logout",
Callback: "/oauth/heroku/callback",
}
}

View File

@ -12,7 +12,6 @@ import (
"github.com/influxdata/chronograf" "github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt" "github.com/influxdata/chronograf/bolt"
"github.com/influxdata/chronograf/canned" "github.com/influxdata/chronograf/canned"
"github.com/influxdata/chronograf/influx"
"github.com/influxdata/chronograf/layouts" "github.com/influxdata/chronograf/layouts"
clog "github.com/influxdata/chronograf/log" clog "github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/oauth2" "github.com/influxdata/chronograf/oauth2"
@ -267,17 +266,15 @@ func openService(boltPath, cannedPath string, logger chronograf.Logger, useAuth
} }
return Service{ return Service{
SourcesStore: db.SourcesStore, TimeSeriesClient: &InfluxClient{},
ServersStore: db.ServersStore, SourcesStore: db.SourcesStore,
UsersStore: db.UsersStore, ServersStore: db.ServersStore,
TimeSeries: &influx.Client{ UsersStore: db.UsersStore,
Logger: logger, LayoutStore: layouts,
}, DashboardsStore: db.DashboardsStore,
LayoutStore: layouts, AlertRulesStore: db.AlertsStore,
DashboardsStore: db.DashboardsStore, Logger: logger,
AlertRulesStore: db.AlertsStore, UseAuth: useAuth,
Logger: logger,
UseAuth: useAuth,
} }
} }

View File

@ -1,18 +1,30 @@
package server package server
import "github.com/influxdata/chronograf" import (
"context"
"strings"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/enterprise"
"github.com/influxdata/chronograf/influx"
)
// Service handles REST calls to the persistence // Service handles REST calls to the persistence
type Service struct { type Service struct {
SourcesStore chronograf.SourcesStore SourcesStore chronograf.SourcesStore
ServersStore chronograf.ServersStore ServersStore chronograf.ServersStore
LayoutStore chronograf.LayoutStore LayoutStore chronograf.LayoutStore
AlertRulesStore chronograf.AlertRulesStore AlertRulesStore chronograf.AlertRulesStore
UsersStore chronograf.UsersStore UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore DashboardsStore chronograf.DashboardsStore
TimeSeries chronograf.TimeSeries TimeSeriesClient TimeSeriesClient
Logger chronograf.Logger Logger chronograf.Logger
UseAuth bool UseAuth bool
}
// TimeSeriesClient returns the correct client for a time series database.
type TimeSeriesClient interface {
New(chronograf.Source, chronograf.Logger) (chronograf.TimeSeries, error)
} }
// ErrorMessage is the error response format for all service errors // ErrorMessage is the error response format for all service errors
@ -20,3 +32,29 @@ type ErrorMessage struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message"` Message string `json:"message"`
} }
// TimeSeries returns a new client connected to a time series database
func (s *Service) TimeSeries(src chronograf.Source) (chronograf.TimeSeries, error) {
return s.TimeSeriesClient.New(src, s.Logger)
}
// InfluxClient returns a new client to connect to OSS or Enterprise
type InfluxClient struct{}
// New creates a client to connect to OSS or enterprise
func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger) (chronograf.TimeSeries, error) {
if src.Type == "influx-enterprise" && src.MetaURL != "" {
dataNode := &influx.Client{
Logger: logger,
}
if err := dataNode.Connect(context.TODO(), &src); err != nil {
return nil, err
}
tls := strings.Contains(src.MetaURL, "https")
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, src.Username, src.Password, tls, dataNode)
}
return &influx.Client{
Logger: logger,
}, nil
}

View File

@ -11,9 +11,12 @@ import (
) )
type sourceLinks struct { type sourceLinks struct {
Self string `json:"self"` // Self link mapping to this resource Self string `json:"self"` // Self link mapping to this resource
Kapacitors string `json:"kapacitors"` // URL for kapacitors endpoint Kapacitors string `json:"kapacitors"` // URL for kapacitors endpoint
Proxy string `json:"proxy"` // URL for proxy endpoint Proxy string `json:"proxy"` // URL for proxy endpoint
Permissions string `json:"permissions"` // URL for all allowed permissions for this source
Users string `json:"users"` // URL for all users associated with this source
Roles string `json:"roles,omitempty"` // URL for all users associated with this source
} }
type sourceResponse struct { type sourceResponse struct {
@ -31,14 +34,21 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
src.Password = "" src.Password = ""
httpAPISrcs := "/chronograf/v1/sources" httpAPISrcs := "/chronograf/v1/sources"
return sourceResponse{ res := sourceResponse{
Source: src, Source: src,
Links: sourceLinks{ Links: sourceLinks{
Self: fmt.Sprintf("%s/%d", httpAPISrcs, src.ID), Self: fmt.Sprintf("%s/%d", httpAPISrcs, src.ID),
Proxy: fmt.Sprintf("%s/%d/proxy", httpAPISrcs, src.ID), Proxy: fmt.Sprintf("%s/%d/proxy", httpAPISrcs, src.ID),
Kapacitors: fmt.Sprintf("%s/%d/kapacitors", httpAPISrcs, src.ID), Kapacitors: fmt.Sprintf("%s/%d/kapacitors", httpAPISrcs, src.ID),
Permissions: fmt.Sprintf("%s/%d/permissions", httpAPISrcs, src.ID),
Users: fmt.Sprintf("%s/%d/users", httpAPISrcs, src.ID),
}, },
} }
if src.Type == "influx-enterprise" {
res.Links.Roles = fmt.Sprintf("%s/%d/roles", httpAPISrcs, src.ID)
}
return res
} }
// NewSource adds a new valid source to the store // NewSource adds a new valid source to the store

View File

@ -25,9 +25,11 @@ func Test_newSourceResponse(t *testing.T) {
Telegraf: "telegraf", Telegraf: "telegraf",
}, },
Links: sourceLinks{ Links: sourceLinks{
Self: "/chronograf/v1/sources/1", Self: "/chronograf/v1/sources/1",
Proxy: "/chronograf/v1/sources/1/proxy", Proxy: "/chronograf/v1/sources/1/proxy",
Kapacitors: "/chronograf/v1/sources/1/kapacitors", Kapacitors: "/chronograf/v1/sources/1/kapacitors",
Users: "/chronograf/v1/sources/1/users",
Permissions: "/chronograf/v1/sources/1/permissions",
}, },
}, },
}, },
@ -43,9 +45,11 @@ func Test_newSourceResponse(t *testing.T) {
Telegraf: "howdy", Telegraf: "howdy",
}, },
Links: sourceLinks{ Links: sourceLinks{
Self: "/chronograf/v1/sources/1", Self: "/chronograf/v1/sources/1",
Proxy: "/chronograf/v1/sources/1/proxy", Proxy: "/chronograf/v1/sources/1/proxy",
Kapacitors: "/chronograf/v1/sources/1/kapacitors", Kapacitors: "/chronograf/v1/sources/1/kapacitors",
Users: "/chronograf/v1/sources/1/users",
Permissions: "/chronograf/v1/sources/1/permissions",
}, },
}, },
}, },

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
package server package server
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -24,120 +24,19 @@ type userResponse struct {
// indicates authentication is not needed // indicates authentication is not needed
func newUserResponse(usr *chronograf.User) userResponse { func newUserResponse(usr *chronograf.User) userResponse {
base := "/chronograf/v1/users" base := "/chronograf/v1/users"
name := "me"
if usr != nil { if usr != nil {
return userResponse{ // TODO: Change to usrl.PathEscape for go 1.8
User: usr, u := &url.URL{Path: usr.Name}
Links: userLinks{ name = u.String()
Self: fmt.Sprintf("%s/%d", base, usr.ID),
},
}
}
return userResponse{}
}
// NewUser adds a new valid user to the store
func (h *Service) NewUser(w http.ResponseWriter, r *http.Request) {
var usr *chronograf.User
if err := json.NewDecoder(r.Body).Decode(usr); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := ValidUserRequest(usr); err != nil {
invalidData(w, err, h.Logger)
return
} }
var err error return userResponse{
if usr, err = h.UsersStore.Add(r.Context(), usr); err != nil { User: usr,
msg := fmt.Errorf("error storing user %v: %v", *usr, err) Links: userLinks{
unknownErrorWithMessage(w, msg, h.Logger) Self: fmt.Sprintf("%s/%s", base, name),
return },
} }
res := newUserResponse(usr)
w.Header().Add("Location", res.Links.Self)
encodeJSON(w, http.StatusCreated, res, h.Logger)
}
// UserID retrieves a user from the store
func (h *Service) UserID(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
ctx := r.Context()
usr, err := h.UsersStore.Get(ctx, chronograf.UserID(id))
if err != nil {
notFound(w, id, h.Logger)
return
}
res := newUserResponse(usr)
encodeJSON(w, http.StatusOK, res, h.Logger)
}
// RemoveUser deletes the user from the store
func (h *Service) RemoveUser(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
usr := &chronograf.User{ID: chronograf.UserID(id)}
ctx := r.Context()
if err = h.UsersStore.Delete(ctx, usr); err != nil {
unknownErrorWithMessage(w, err, h.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
// UpdateUser handles incremental updates of a data user
func (h *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
ctx := r.Context()
usr, err := h.UsersStore.Get(ctx, chronograf.UserID(id))
if err != nil {
notFound(w, id, h.Logger)
return
}
var req chronograf.User
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
usr.Email = req.Email
if err := ValidUserRequest(usr); err != nil {
invalidData(w, err, h.Logger)
return
}
if err := h.UsersStore.Update(ctx, usr); err != nil {
msg := fmt.Sprintf("Error updating user ID %d", id)
Error(w, http.StatusInternalServerError, msg, h.Logger)
return
}
encodeJSON(w, http.StatusOK, newUserResponse(usr), h.Logger)
}
// ValidUserRequest checks if email is nonempty
func ValidUserRequest(s *chronograf.User) error {
// email is required
if s.Email == "" {
return fmt.Errorf("Email required")
}
return nil
} }
func getEmail(ctx context.Context) (string, error) { func getEmail(ctx context.Context) (string, error) {
@ -169,12 +68,14 @@ func (h *Service) Me(w http.ResponseWriter, r *http.Request) {
encodeJSON(w, http.StatusOK, res, h.Logger) encodeJSON(w, http.StatusOK, res, h.Logger)
return return
} }
email, err := getEmail(ctx) email, err := getEmail(ctx)
if err != nil { if err != nil {
invalidData(w, err, h.Logger) invalidData(w, err, h.Logger)
return return
} }
usr, err := h.UsersStore.FindByEmail(ctx, email)
usr, err := h.UsersStore.Get(ctx, email)
if err == nil { if err == nil {
res := newUserResponse(usr) res := newUserResponse(usr)
encodeJSON(w, http.StatusOK, res, h.Logger) encodeJSON(w, http.StatusOK, res, h.Logger)
@ -183,15 +84,16 @@ func (h *Service) Me(w http.ResponseWriter, r *http.Request) {
// Because we didnt find a user, making a new one // Because we didnt find a user, making a new one
user := &chronograf.User{ user := &chronograf.User{
Email: email, Name: email,
} }
user, err = h.UsersStore.Add(ctx, user)
newUser, err := h.UsersStore.Add(ctx, user)
if err != nil { if err != nil {
msg := fmt.Errorf("error storing user %v: %v", user, err) msg := fmt.Errorf("error storing user %s: %v", user.Name, err)
unknownErrorWithMessage(w, msg, h.Logger) unknownErrorWithMessage(w, msg, h.Logger)
return return
} }
res := newUserResponse(user) res := newUserResponse(newUser)
encodeJSON(w, http.StatusOK, res, h.Logger) encodeJSON(w, http.StatusOK, res, h.Logger)
} }

168
server/users_test.go Normal file
View File

@ -0,0 +1,168 @@
package server
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/oauth2"
)
type MockUsers struct{}
func TestService_Me(t *testing.T) {
type fields struct {
UsersStore chronograf.UsersStore
Logger chronograf.Logger
UseAuth bool
}
type args struct {
w *httptest.ResponseRecorder
r *http.Request
}
tests := []struct {
name string
fields fields
args args
principal oauth2.Principal
wantStatus int
wantContentType string
wantBody string
}{
{
name: "Existing user",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
return &chronograf.User{
Name: "me",
Passwd: "hunter2",
}, nil
},
},
},
principal: oauth2.Principal{
Subject: "me",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"me","password":"hunter2","links":{"self":"/chronograf/v1/users/me"}}
`,
},
{
name: "New user",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
return nil, fmt.Errorf("Unknown User")
},
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return u, nil
},
},
},
principal: oauth2.Principal{
Subject: "secret",
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","password":"","links":{"self":"/chronograf/v1/users/secret"}}
`,
},
{
name: "Error adding user",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, name string) (*chronograf.User, error) {
return nil, fmt.Errorf("Unknown User")
},
AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
return nil, fmt.Errorf("Why Heavy?")
},
},
Logger: log.New(log.DebugLevel),
},
principal: oauth2.Principal{
Subject: "secret",
},
wantStatus: http.StatusInternalServerError,
wantContentType: "application/json",
wantBody: `{"code":500,"message":"Unknown error: error storing user secret: Why Heavy?"}`,
},
{
name: "No Auth",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: false,
Logger: log.New(log.DebugLevel),
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"links":{"self":"/chronograf/v1/users/me"}}
`,
},
{
name: "Empty Principal",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
},
wantStatus: http.StatusUnprocessableEntity,
principal: oauth2.Principal{
Subject: "",
},
},
}
for _, tt := range tests {
tt.args.r = tt.args.r.WithContext(context.WithValue(context.Background(), oauth2.PrincipalKey, tt.principal))
h := &Service{
UsersStore: tt.fields.UsersStore,
Logger: tt.fields.Logger,
UseAuth: tt.fields.UseAuth,
}
h.Me(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
content := resp.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode != tt.wantStatus {
t.Errorf("%q. Me() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus)
}
if tt.wantContentType != "" && content != tt.wantContentType {
t.Errorf("%q. Me() = %v, want %v", tt.name, content, tt.wantContentType)
}
if tt.wantBody != "" && string(body) != tt.wantBody {
t.Errorf("%q. Me() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody)
}
}
}

View File

@ -47,7 +47,7 @@ const DashboardHeader = ({
<ReactTooltip id="graph-tips-tooltip" effect="solid" html={true} offset={{top: 2}} place="bottom" class="influx-tooltip place-bottom" /> <ReactTooltip id="graph-tips-tooltip" effect="solid" html={true} offset={{top: 2}} place="bottom" class="influx-tooltip place-bottom" />
<TimeRangeDropdown onChooseTimeRange={handleChooseTimeRange} selected={timeRange.inputValue} /> <TimeRangeDropdown onChooseTimeRange={handleChooseTimeRange} selected={timeRange.inputValue} />
<div className="btn btn-info btn-sm" onClick={handleClickPresentationButton}> <div className="btn btn-info btn-sm" onClick={handleClickPresentationButton}>
<span className="icon keynote" style={{margin: 0}}></span> <span className="icon expand-a" style={{margin: 0}}></span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -174,7 +174,7 @@ export default React.createClass({
render() { render() {
return ( return (
<div ref="self"> <div ref="self" style={{height: '100%'}}>
<div ref="graphContainer" style={this.props.containerStyle} /> <div ref="graphContainer" style={this.props.containerStyle} />
<div className="container--dygraph-legend" ref="legendContainer" /> <div className="container--dygraph-legend" ref="legendContainer" />
<div className="graph-vertical-marker" ref="graphVerticalMarker" /> <div className="graph-vertical-marker" ref="graphVerticalMarker" />

View File

@ -104,7 +104,10 @@ export default React.createClass({
} }
return ( return (
<div className={classNames({"graph--hasYLabel": !!(options.ylabel || options.y2label)})}> <div
className={classNames({"graph--hasYLabel": !!(options.ylabel || options.y2label)})}
style={{height: '100%'}}
>
{isRefreshing ? this.renderSpinner() : null} {isRefreshing ? this.renderSpinner() : null}
<Dygraph <Dygraph
containerStyle={{width: '100%', height: '100%'}} containerStyle={{width: '100%', height: '100%'}}

View File

@ -49,6 +49,7 @@ const TimeRangeDropdown = React.createClass({
<span className="caret" /> <span className="caret" />
</div> </div>
<ul className={cN("dropdown-menu", {show: isOpen})}> <ul className={cN("dropdown-menu", {show: isOpen})}>
<li className="dropdown-header">Time Range</li>
{timeRanges.map((item) => { {timeRanges.map((item) => {
return ( return (
<li key={item.menuOption}> <li key={item.menuOption}>

View File

@ -468,5 +468,5 @@ export const STROKE_WIDTH = {
light: 1.5, light: 1.5,
}; };
export const PRESENTATION_MODE_ANIMATION_DELAY = 250 // In milliseconds. export const PRESENTATION_MODE_ANIMATION_DELAY = 0 // In milliseconds.
export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds. export const PRESENTATION_MODE_NOTIFICATION_DELAY = 2000 // In milliseconds.

View File

@ -0,0 +1,10 @@
// Trigger resize event to relayout the React Layout plugin
export default function resizeLayout() {
return next => action => {
next(action);
if (action.type === 'ENABLE_PRESENTATION_MODE' || action.type === 'DISABLE_PRESENTATION_MODE') {
window.dispatchEvent(new Event('resize'));
}
}
}

View File

@ -2,6 +2,7 @@ import {createStore, applyMiddleware, compose} from 'redux';
import {combineReducers} from 'redux'; import {combineReducers} from 'redux';
import thunkMiddleware from 'redux-thunk'; import thunkMiddleware from 'redux-thunk';
import makeQueryExecuter from 'src/shared/middleware/queryExecuter'; import makeQueryExecuter from 'src/shared/middleware/queryExecuter';
import resizeLayout from 'src/shared/middleware/resizeLayout';
import * as dataExplorerReducers from 'src/data_explorer/reducers'; import * as dataExplorerReducers from 'src/data_explorer/reducers';
import * as sharedReducers from 'src/shared/reducers'; import * as sharedReducers from 'src/shared/reducers';
import rulesReducer from 'src/kapacitor/reducers/rules'; import rulesReducer from 'src/kapacitor/reducers/rules';
@ -20,7 +21,7 @@ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export default function configureStore(initialState) { export default function configureStore(initialState) {
const createPersistentStore = composeEnhancers( const createPersistentStore = composeEnhancers(
persistStateEnhancer(), persistStateEnhancer(),
applyMiddleware(thunkMiddleware, makeQueryExecuter()), applyMiddleware(thunkMiddleware, makeQueryExecuter(), resizeLayout),
)(createStore); )(createStore);

View File

@ -18,6 +18,7 @@
border-color: $g5-pepper; border-color: $g5-pepper;
border-width: 2px; border-width: 2px;
background-color: $g3-castle; background-color: $g3-castle;
white-space: nowrap;
} }
.dropdown-toggle { .dropdown-toggle {
border-radius: 0px 3px 3px 0; border-radius: 0px 3px 3px 0;

View File

@ -1,6 +1,6 @@
.multi-select-dropdown { .multi-select-dropdown {
.dropdown-toggle { .dropdown-toggle {
width: 150px; width: 110px;
} }
&__apply { &__apply {
margin: 0; margin: 0;

Binary file not shown.

View File

@ -82,6 +82,7 @@
<glyph unicode="&#xe946;" glyph-name="redo" horiz-adv-x="908" d="M838.749 199.68c-49.804 13.498-100.538-16.291-114.036-65.629-39.098-146.153-189.905-233.193-335.593-194.095-146.153 39.098-233.193 189.905-194.095 336.524 33.513 123.345 146.618 204.8 269.498 202.938l-9.309-65.164c-3.258-22.807 6.982-45.615 26.065-58.647 10.24-6.982 21.876-10.24 33.513-10.24 10.705 0 20.945 2.793 30.72 8.378l209.455 123.345c15.825 9.309 26.531 25.135 29.324 43.753 2.793 18.153-3.258 36.771-15.825 50.269l-167.564 178.269c-15.825 16.756-40.029 23.273-62.371 16.291s-38.167-26.065-41.425-49.338l-6.982-51.2c-214.575 13.964-416.582-125.207-474.298-341.178-65.629-245.295 80.058-498.502 325.353-564.596 39.564-10.705 79.593-15.825 119.156-15.825 202.938 0 389.585 135.913 444.509 341.644 13.033 50.269-16.756 101.004-66.095 114.502z" /> <glyph unicode="&#xe946;" glyph-name="redo" horiz-adv-x="908" d="M838.749 199.68c-49.804 13.498-100.538-16.291-114.036-65.629-39.098-146.153-189.905-233.193-335.593-194.095-146.153 39.098-233.193 189.905-194.095 336.524 33.513 123.345 146.618 204.8 269.498 202.938l-9.309-65.164c-3.258-22.807 6.982-45.615 26.065-58.647 10.24-6.982 21.876-10.24 33.513-10.24 10.705 0 20.945 2.793 30.72 8.378l209.455 123.345c15.825 9.309 26.531 25.135 29.324 43.753 2.793 18.153-3.258 36.771-15.825 50.269l-167.564 178.269c-15.825 16.756-40.029 23.273-62.371 16.291s-38.167-26.065-41.425-49.338l-6.982-51.2c-214.575 13.964-416.582-125.207-474.298-341.178-65.629-245.295 80.058-498.502 325.353-564.596 39.564-10.705 79.593-15.825 119.156-15.825 202.938 0 389.585 135.913 444.509 341.644 13.033 50.269-16.756 101.004-66.095 114.502z" />
<glyph unicode="&#xe947;" glyph-name="heroku" d="M819.2 768h-614.4c-43.52 0-76.8-33.28-76.8-76.8v-870.4c0-43.52 33.28-76.8 76.8-76.8h614.4c43.52 0 76.8 33.28 76.8 76.8v870.4c0 43.52-33.28 76.8-76.8 76.8zM399.36-2.56l-115.2-92.16c0 0-2.56-2.56-5.12-2.56 0 0-2.56 0-2.56 0-2.56 0-2.56 2.56-2.56 5.12v194.56c0 2.56 2.56 5.12 2.56 5.12 2.56 0 5.12 0 7.68 0l115.2-102.4c2.56 0 2.56-2.56 2.56-5.12 0 0 0-2.56-2.56-2.56zM750.080-89.6c0-2.56-2.56-5.12-5.12-5.12h-115.2c-2.56 0-5.12 2.56-5.12 5.12v268.8c0 23.040 0 43.52-15.36 56.32-12.8 10.24-35.84 12.8-71.68 7.68-46.080-7.68-97.28-20.48-143.36-33.28-2.56 0-5.12-2.56-7.68-2.56s-7.68-2.56-10.24-2.56c-2.56 0-5.12-2.56-7.68-2.56s-7.68-2.56-10.24-2.56c-2.56 0-5.12-2.56-7.68-2.56s-5.12-2.56-7.68-2.56c-2.56 0-5.12-2.56-7.68-2.56s-5.12-2.56-7.68-2.56c-2.56 0-5.12-2.56-7.68-2.56s-2.56-2.56-5.12-2.56-5.12-2.56-7.68-5.12c0 0-2.56 0-2.56-2.56-2.56-2.56-5.12-2.56-7.68-5.12 0 0-2.56 0-2.56 0-2.56-2.56-5.12-5.12-7.68-5.12 0 0-2.56 0-2.56 0s0 0 0 0 0 0 0 0-2.56 0-2.56 0c0 0 0 0 0 0s0 0 0 0 0 0 0 0 0 2.56 0 2.56c0 0 0 0 0 2.56 0 0 0 0 0 0v432.64c0 2.56 2.56 5.12 5.12 5.12h115.2c2.56 0 5.12-2.56 5.12-5.12v-261.12c58.88 17.92 138.24 33.28 212.48 23.040 81.92-10.24 135.68-48.64 135.68-189.44v-266.24zM668.16 435.2c0-2.56-2.56-2.56-5.12-2.56h-115.2c-2.56 0-5.12 2.56-5.12 2.56s0 5.12 0 7.68c28.16 38.4 79.36 107.52 79.36 158.72 0 2.56 2.56 5.12 5.12 5.12h115.2c2.56 0 5.12-2.56 5.12-5.12 2.56-53.76-51.2-125.44-79.36-166.4z" /> <glyph unicode="&#xe947;" glyph-name="heroku" d="M819.2 768h-614.4c-43.52 0-76.8-33.28-76.8-76.8v-870.4c0-43.52 33.28-76.8 76.8-76.8h614.4c43.52 0 76.8 33.28 76.8 76.8v870.4c0 43.52-33.28 76.8-76.8 76.8zM399.36-2.56l-115.2-92.16c0 0-2.56-2.56-5.12-2.56 0 0-2.56 0-2.56 0-2.56 0-2.56 2.56-2.56 5.12v194.56c0 2.56 2.56 5.12 2.56 5.12 2.56 0 5.12 0 7.68 0l115.2-102.4c2.56 0 2.56-2.56 2.56-5.12 0 0 0-2.56-2.56-2.56zM750.080-89.6c0-2.56-2.56-5.12-5.12-5.12h-115.2c-2.56 0-5.12 2.56-5.12 5.12v268.8c0 23.040 0 43.52-15.36 56.32-12.8 10.24-35.84 12.8-71.68 7.68-46.080-7.68-97.28-20.48-143.36-33.28-2.56 0-5.12-2.56-7.68-2.56s-7.68-2.56-10.24-2.56c-2.56 0-5.12-2.56-7.68-2.56s-7.68-2.56-10.24-2.56c-2.56 0-5.12-2.56-7.68-2.56s-5.12-2.56-7.68-2.56c-2.56 0-5.12-2.56-7.68-2.56s-5.12-2.56-7.68-2.56c-2.56 0-5.12-2.56-7.68-2.56s-2.56-2.56-5.12-2.56-5.12-2.56-7.68-5.12c0 0-2.56 0-2.56-2.56-2.56-2.56-5.12-2.56-7.68-5.12 0 0-2.56 0-2.56 0-2.56-2.56-5.12-5.12-7.68-5.12 0 0-2.56 0-2.56 0s0 0 0 0 0 0 0 0-2.56 0-2.56 0c0 0 0 0 0 0s0 0 0 0 0 0 0 0 0 2.56 0 2.56c0 0 0 0 0 2.56 0 0 0 0 0 0v432.64c0 2.56 2.56 5.12 5.12 5.12h115.2c2.56 0 5.12-2.56 5.12-5.12v-261.12c58.88 17.92 138.24 33.28 212.48 23.040 81.92-10.24 135.68-48.64 135.68-189.44v-266.24zM668.16 435.2c0-2.56-2.56-2.56-5.12-2.56h-115.2c-2.56 0-5.12 2.56-5.12 2.56s0 5.12 0 7.68c28.16 38.4 79.36 107.52 79.36 158.72 0 2.56 2.56 5.12 5.12 5.12h115.2c2.56 0 5.12-2.56 5.12-5.12 2.56-53.76-51.2-125.44-79.36-166.4z" />
<glyph unicode="&#xe948;" glyph-name="heroku-simple" d="M348.16-120.32l-166.4-133.12c-2.56-2.56-2.56-2.56-5.12-2.56s-2.56 0-5.12 0c-2.56 2.56-5.12 5.12-5.12 7.68v281.6c0 2.56 2.56 7.68 5.12 7.68 2.56 2.56 7.68 0 10.24-2.56l166.4-148.48c2.56-2.56 2.56-5.12 2.56-7.68 2.56 0 0-2.56-2.56-2.56zM857.6-245.76c0-5.12-5.12-10.24-10.24-10.24h-166.4c-5.12 0-10.24 5.12-10.24 10.24v391.68c0 30.72 0 64-20.48 81.92-17.92 15.36-53.76 20.48-102.4 12.8-66.56-10.24-140.8-28.16-207.36-48.64-5.12 0-7.68-2.56-10.24-2.56-5.12-2.56-10.24-2.56-15.36-5.12-2.56 0-7.68-2.56-10.24-2.56-5.12-2.56-10.24-2.56-12.8-5.12-2.56 0-7.68-2.56-10.24-2.56-5.12-2.56-7.68-2.56-12.8-5.12-2.56-2.56-7.68-2.56-10.24-5.12s-7.68-2.56-10.24-5.12c-5.12-2.56-7.68-2.56-12.8-5.12-2.56 0-5.12-2.56-7.68-2.56-5.12-2.56-7.68-5.12-12.8-7.68-2.56 0-2.56-2.56-5.12-2.56-5.12-2.56-7.68-5.12-12.8-7.68 0 0-2.56 0-2.56-2.56-5.12-2.56-7.68-5.12-12.8-7.68-2.56 0-2.56-2.56-5.12-2.56 0 0 0 0 0 0s0 0 0 0-2.56 0-2.56 0c0 0 0 0 0 0s0 0 0 0 0 0 0 0-2.56 2.56-2.56 2.56c0 0 0 0 0 2.56 0 0 0 0 0 0v629.76c0 5.12 5.12 10.24 10.24 10.24h166.4c5.12 0 10.24-5.12 10.24-10.24v-378.88c87.040 25.6 202.24 48.64 307.2 33.28 117.76-15.36 197.12-69.12 197.12-276.48v-378.88zM739.84 517.12c-2.56-2.56-5.12-5.12-7.68-5.12h-166.4c-2.56 0-7.68 2.56-7.68 5.12s-2.56 7.68 0 10.24c40.96 56.32 115.2 158.72 115.2 230.4 0 5.12 5.12 10.24 10.24 10.24h166.4c5.12 0 10.24-5.12 10.24-10.24-2.56-76.8-79.36-181.76-120.32-240.64z" /> <glyph unicode="&#xe948;" glyph-name="heroku-simple" d="M348.16-120.32l-166.4-133.12c-2.56-2.56-2.56-2.56-5.12-2.56s-2.56 0-5.12 0c-2.56 2.56-5.12 5.12-5.12 7.68v281.6c0 2.56 2.56 7.68 5.12 7.68 2.56 2.56 7.68 0 10.24-2.56l166.4-148.48c2.56-2.56 2.56-5.12 2.56-7.68 2.56 0 0-2.56-2.56-2.56zM857.6-245.76c0-5.12-5.12-10.24-10.24-10.24h-166.4c-5.12 0-10.24 5.12-10.24 10.24v391.68c0 30.72 0 64-20.48 81.92-17.92 15.36-53.76 20.48-102.4 12.8-66.56-10.24-140.8-28.16-207.36-48.64-5.12 0-7.68-2.56-10.24-2.56-5.12-2.56-10.24-2.56-15.36-5.12-2.56 0-7.68-2.56-10.24-2.56-5.12-2.56-10.24-2.56-12.8-5.12-2.56 0-7.68-2.56-10.24-2.56-5.12-2.56-7.68-2.56-12.8-5.12-2.56-2.56-7.68-2.56-10.24-5.12s-7.68-2.56-10.24-5.12c-5.12-2.56-7.68-2.56-12.8-5.12-2.56 0-5.12-2.56-7.68-2.56-5.12-2.56-7.68-5.12-12.8-7.68-2.56 0-2.56-2.56-5.12-2.56-5.12-2.56-7.68-5.12-12.8-7.68 0 0-2.56 0-2.56-2.56-5.12-2.56-7.68-5.12-12.8-7.68-2.56 0-2.56-2.56-5.12-2.56 0 0 0 0 0 0s0 0 0 0-2.56 0-2.56 0c0 0 0 0 0 0s0 0 0 0 0 0 0 0-2.56 2.56-2.56 2.56c0 0 0 0 0 2.56 0 0 0 0 0 0v629.76c0 5.12 5.12 10.24 10.24 10.24h166.4c5.12 0 10.24-5.12 10.24-10.24v-378.88c87.040 25.6 202.24 48.64 307.2 33.28 117.76-15.36 197.12-69.12 197.12-276.48v-378.88zM739.84 517.12c-2.56-2.56-5.12-5.12-7.68-5.12h-166.4c-2.56 0-7.68 2.56-7.68 5.12s-2.56 7.68 0 10.24c40.96 56.32 115.2 158.72 115.2 230.4 0 5.12 5.12 10.24 10.24 10.24h166.4c5.12 0 10.24-5.12 10.24-10.24-2.56-76.8-79.36-181.76-120.32-240.64z" />
<glyph unicode="&#xe949;" glyph-name="refresh" d="M417.28 442.88l-261.12 28.16c-15.36 2.56-25.6 15.36-25.6 30.72l28.16 261.12c0 7.68 10.24 10.24 17.92 5.12l248.32-307.2c5.12-10.24 0-20.48-7.68-17.92zM865.28-248.32l28.16 261.12c2.56 15.36-10.24 28.16-25.6 30.72l-261.12 28.16c-7.68 0-12.8-7.68-7.68-15.36l248.32-307.2c5.12-7.68 15.36-5.12 17.92 2.56zM834.56 578.56c-87.040 87.040-202.24 133.12-322.56 133.12-110.080 0-217.6-40.96-302.080-112.64l-10.24-10.24 58.88-58.88 10.24 7.68c84.48 74.24 199.68 104.96 314.88 81.92 51.2-10.24 99.84-30.72 143.36-61.44 130.56-94.72 186.88-253.44 145.92-404.48-5.12-20.48 5.12-43.52 23.040-51.2 5.12-2.56 10.24-2.56 17.92-2.56 5.12 0 12.8 2.56 17.92 5.12 10.24 5.12 20.48 15.36 23.040 28.16 43.52 161.28-2.56 327.68-120.32 445.44zM755.2-28.16c-84.48-74.24-199.68-104.96-314.88-81.92-51.2 10.24-99.84 30.72-143.36 61.44-130.56 94.72-186.88 250.88-145.92 404.48 5.12 20.48-5.12 43.52-23.040 51.2-10.24 5.12-23.040 5.12-35.84 0-10.24-5.12-17.92-15.36-20.48-25.6-46.080-161.28-2.56-327.68 117.76-445.44 87.040-87.040 202.24-133.12 322.56-133.12 110.080 0 217.6 40.96 302.080 112.64l10.24 10.24-58.88 58.88-10.24-12.8z" />
<glyph unicode="&#xea88;" glyph-name="google" d="M522.2 329.2v-175.6h290.4c-11.8-75.4-87.8-220.8-290.4-220.8-174.8 0-317.4 144.8-317.4 323.2s142.6 323.2 317.4 323.2c99.4 0 166-42.4 204-79l139 133.8c-89.2 83.6-204.8 134-343 134-283 0-512-229-512-512s229-512 512-512c295.4 0 491.6 207.8 491.6 500.2 0 33.6-3.6 59.2-8 84.8l-483.6 0.2z" /> <glyph unicode="&#xea88;" glyph-name="google" d="M522.2 329.2v-175.6h290.4c-11.8-75.4-87.8-220.8-290.4-220.8-174.8 0-317.4 144.8-317.4 323.2s142.6 323.2 317.4 323.2c99.4 0 166-42.4 204-79l139 133.8c-89.2 83.6-204.8 134-343 134-283 0-512-229-512-512s229-512 512-512c295.4 0 491.6 207.8 491.6 500.2 0 33.6-3.6 59.2-8 84.8l-483.6 0.2z" />
<glyph unicode="&#xea8a;" glyph-name="google3" d="M512 768c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM519.6-128c-212.2 0-384 171.8-384 384s171.8 384 384 384c103.6 0 190.4-37.8 257.2-100.4l-104.2-100.4c-28.6 27.4-78.4 59.2-153 59.2-131.2 0-238-108.6-238-242.4s107-242.4 238-242.4c152 0 209 109.2 217.8 165.6h-217.8v131.6h362.6c3.2-19.2 6-38.4 6-63.6 0.2-219.4-146.8-375.2-368.6-375.2z" /> <glyph unicode="&#xea8a;" glyph-name="google3" d="M512 768c-282.8 0-512-229.2-512-512s229.2-512 512-512 512 229.2 512 512-229.2 512-512 512zM519.6-128c-212.2 0-384 171.8-384 384s171.8 384 384 384c103.6 0 190.4-37.8 257.2-100.4l-104.2-100.4c-28.6 27.4-78.4 59.2-153 59.2-131.2 0-238-108.6-238-242.4s107-242.4 238-242.4c152 0 209 109.2 217.8 165.6h-217.8v131.6h362.6c3.2-19.2 6-38.4 6-63.6 0.2-219.4-146.8-375.2-368.6-375.2z" />
<glyph unicode="&#xea8b;" glyph-name="google-plus" d="M325.8 310.6v-111.8h184.8c-7.4-48-55.8-140.6-184.8-140.6-111.2 0-202 92.2-202 205.8s90.8 205.8 202 205.8c63.4 0 105.6-27 129.8-50.2l88.4 85.2c-56.8 53-130.4 85.2-218.2 85.2-180.2-0.2-325.8-145.8-325.8-326s145.6-325.8 325.8-325.8c188 0 312.8 132.2 312.8 318.4 0 21.4-2.4 37.8-5.2 54h-307.6zM1024 320h-96v96h-96v-96h-96v-96h96v-96h96v96h96z" /> <glyph unicode="&#xea8b;" glyph-name="google-plus" d="M325.8 310.6v-111.8h184.8c-7.4-48-55.8-140.6-184.8-140.6-111.2 0-202 92.2-202 205.8s90.8 205.8 202 205.8c63.4 0 105.6-27 129.8-50.2l88.4 85.2c-56.8 53-130.4 85.2-218.2 85.2-180.2-0.2-325.8-145.8-325.8-326s145.6-325.8 325.8-325.8c188 0 312.8 132.2 312.8 318.4 0 21.4-2.4 37.8-5.2 54h-307.6zM1024 320h-96v96h-96v-96h-96v-96h96v-96h96v96h96z" />

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -43,3 +43,6 @@ $de-graph-heading-height: 44px;
@import 'data-explorer/raw-text'; @import 'data-explorer/raw-text';
@import 'data-explorer/tag-list'; @import 'data-explorer/tag-list';
@import 'data-explorer/visualization'; @import 'data-explorer/visualization';
// Font size in response to screen size
@import 'data-explorer/font-scale';

View File

@ -0,0 +1,82 @@
$breakpoint-a: 1500px;
$breakpoint-b: 1800px;
$breakpoint-c: 2100px;
@media only screen and (min-width: $breakpoint-a) {
.data-explorer {
.qeditor--list-item,
.query-builder--tab-label {
font-size: 14px;
}
.query-builder--tab {
height: 38px;
}
.graph-title {
font-size: 16px;
}
.query-builder--column-heading {
font-size: 15px;
}
}
}
@media only screen and (min-width: $breakpoint-b) {
.data-explorer {
.query-builder--tabs {
width: 320px;
}
.qeditor--list-item,
.query-builder--tab-label {
font-size: 15px;
}
.query-builder--tab {
height: 42px;
}
.query-builder--column-heading {
font-size: 16px;
}
.query-builder--query-preview pre code {
font-size: 13.5px;
}
.toggle-sm .toggle-btn {
font-size: 14px;
}
.btn-xs {
font-size: 13.5px;
}
}
}
@media only screen and (min-width: $breakpoint-c) {
.data-explorer {
.query-builder--tabs {
width: 372px;
}
.qeditor--list-item {
letter-spacing: 0.3px;
}
.query-builder--tab-label {
font-size: 16px;
}
.query-builder--tab {
height: 46px;
}
.query-builder--column-heading {
font-size: 17px;
font-weight: 400;
text-transform: uppercase;
}
.query-builder--query-preview pre code {
font-size: 14px;
}
.toggle-sm .toggle-btn {
font-size: 14px;
}
.btn-xs {
font-size: 14px;
}
.multi-select-dropdown .dropdown-toggle {
width: 140px;
}
}
}

View File

@ -10,6 +10,8 @@
position: relative; position: relative;
pre { pre {
display: flex;
align-items: center;
padding: 9px; padding: 9px;
border: 0; border: 0;
background-color: $query-editor-tab-inactive; background-color: $query-editor-tab-inactive;
@ -169,7 +171,7 @@
position: absolute; position: absolute;
top: 15px; top: 15px;
right: 16px; right: 16px;
width: calc(60% - 16px); width: calc(50% - 16px);
height: 30px; height: 30px;
padding: 0; padding: 0;
z-index: 10; z-index: 10;

View File

@ -98,8 +98,8 @@
background-color: $g3-castle; background-color: $g3-castle;
border: 2px solid $g5-pepper; border: 2px solid $g5-pepper;
color: $g13-mist; color: $g13-mist;
height: 24px; height: 30px;
border-radius: 12px; border-radius: 15px;
font-size: 13px; font-size: 13px;
padding-left: 25px; padding-left: 25px;
outline: none; outline: none;

View File

@ -37,9 +37,4 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
/* Hacky way to ensure that legends cannot be obscured by neighboring graphs */
div:not(.dashboard-edit) .react-grid-item:hover {
z-index: 8999;
}

View File

@ -37,9 +37,19 @@ $kapacitor-font-sm: 13px;
height: (300px + ($kap-padding-sm * 2)); height: (300px + ($kap-padding-sm * 2));
position: relative; position: relative;
> div { & > div {
padding: 8px 16px; position: absolute;
position: relative; top: 0;
left: $kap-padding-sm;
width: calc(100% - #{($kap-padding-sm * 2)});
height: 100%;
& > div {
position: absolute;
width: 100%;
height: 100%;
padding: 8px 16px;
}
} }
&:before { &:before {
@ -417,12 +427,14 @@ div.qeditor.kapacitor-metric-selector {
} }
.alert-message--endpoint { .alert-message--endpoint {
display: flex;
align-items: center;
width: auto; width: auto;
border-top: 2px solid $kapacitor-divider-color; border-top: 2px solid $kapacitor-divider-color;
> p { & > div {
display: flex;
align-items: center;
}
p {
margin-right: $kap-padding-sm !important; margin-right: $kap-padding-sm !important;
} }
} }

View File

@ -266,6 +266,9 @@
.icon.user-outline:before { .icon.user-outline:before {
content: "\e91c"; content: "\e91c";
} }
.icon.refresh:before {
content: "\e949";
}
.icon.clock:before { .icon.clock:before {
content: "\e91b"; content: "\e91b";
} }

View File

@ -261,6 +261,24 @@ input {
} }
} }
} }
.dropdown-header {
height: 32px;
line-height: 30px;
padding: 0 9px;
white-space: nowrap;
font-size: 14px !important;
font-weight: 900;
color: $c-neutrino !important;
text-transform: none !important;
border-bottom: 2px solid $c-pool;
background-color: $c-ocean;
&:hover {
background-image: none !important;
background-color: $c-ocean !important;
cursor: default;
}
}
/* Dropdown Actions */ /* Dropdown Actions */
.dropdown-item { .dropdown-item {