Merge branch 'master' into release-beta4
commit
abaa2bdff2
|
@ -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]
|
||||||
|
|
||||||
|
|
9
Makefile
9
Makefile
|
@ -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 .
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
113
bolt/users.go
113
bolt/users.go
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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\""
|
||||||
],
|
],
|
||||||
|
|
|
@ -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\""
|
||||||
],
|
],
|
||||||
|
|
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -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\""
|
||||||
],
|
],
|
||||||
|
|
|
@ -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\""
|
||||||
]
|
]
|
||||||
|
|
|
@ -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": []
|
||||||
},
|
},
|
||||||
|
|
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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{},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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 who’s 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)
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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 */
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
134
server/users.go
134
server/users.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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%'}}
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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.
|
@ -82,6 +82,7 @@
|
||||||
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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.
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue