migrate(platform): move public dependencies into platform

migrate(context): move context package to platform

migrate(kit/errors): move errors package to platform

migrate(mock): move mock package to platform

migrate(prometheus): move prometheus package to platform

migrate(rand): move rand package to platform

migrate(snowflake): move snowflake package to platform

migrate(testing): move testing package to platform

migrate(zap): move zap package to platform

migrate(http): move http package to platform

migrate(platform): rename base package idpe to platform

git(base): add gitignore

dep: add dependencies

migrate(platform): remove consumer.go

migrate(platform): make secret things private again
pull/10616/head
Michael Desa 2018-05-14 12:26:38 -04:00
parent 6982acfa37
commit eabba6986d
32 changed files with 4064 additions and 4 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
vendor
.netrc
# Project binaries.
/idp
/transpilerd

163
Gopkg.lock generated Normal file
View File

@ -0,0 +1,163 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/beorn7/perks"
packages = ["quantile"]
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
name = "github.com/gogo/protobuf"
packages = ["proto"]
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
version = "v1.0.0"
[[projects]]
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
[[projects]]
name = "github.com/google/go-cmp"
packages = [
"cmp",
"cmp/internal/diff",
"cmp/internal/function",
"cmp/internal/value"
]
revision = "3af367b6b30c263d47e8895973edcca9a49cf029"
version = "v0.2.0"
[[projects]]
branch = "master"
name = "github.com/influxdata/ifql"
packages = [
"ast",
"compiler",
"id",
"interpreter",
"parser",
"query",
"query/execute",
"query/plan",
"semantic",
"values"
]
revision = "66973752cd65cd99dc43332a94272e250672d448"
[[projects]]
branch = "master"
name = "github.com/influxdata/influxdb"
packages = [
"models",
"pkg/escape",
"pkg/snowflake"
]
revision = "200fda999f915dfc13c2b4030a2db6e0b08c689f"
[[projects]]
name = "github.com/julienschmidt/httprouter"
packages = ["."]
revision = "d1898390779332322e6b5ca5011da4bf249bb056"
[[projects]]
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
name = "github.com/opentracing/opentracing-go"
packages = [
".",
"log"
]
revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
version = "v1.0.2"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/promhttp"
]
revision = "661e31bf844dfca9aeba15f27ea8aa0d485ad212"
[[projects]]
branch = "master"
name = "github.com/prometheus/client_model"
packages = ["go"]
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
]
revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
[[projects]]
branch = "master"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
]
revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
[[projects]]
name = "github.com/satori/go.uuid"
packages = ["."]
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
name = "go.uber.org/atomic"
packages = ["."]
revision = "4e336646b2ef9fc6e47be8e21594178f98e5ebcf"
version = "v1.2.0"
[[projects]]
name = "go.uber.org/multierr"
packages = ["."]
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
version = "v1.1.0"
[[projects]]
name = "go.uber.org/zap"
packages = [
".",
"buffer",
"internal/bufferpool",
"internal/color",
"internal/exit",
"zapcore"
]
revision = "eeedf312bc6c57391d84767a4cd413f02a917974"
version = "v1.8.0"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = ["context"]
revision = "2491c5de3490fced2f6cff376127c667efeed857"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "9a6351899a346f8d38fe7215b78df522799b8a8aefb6619c9025a1ace2549126"
solver-name = "gps-cdcl"
solver-version = 1

62
Gopkg.toml Normal file
View File

@ -0,0 +1,62 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/gogo/protobuf"
version = "1.0.0"
[[constraint]]
name = "github.com/google/go-cmp"
version = "0.2.0"
[[constraint]]
name = "github.com/influxdata/ifql"
branch = "master"
[[constraint]]
name = "github.com/influxdata/influxdb"
branch = "master"
# Dependency is pinned to explicit revision rather than latest release because
# latest release pre-dates context, which we need.
[[constraint]]
name = "github.com/julienschmidt/httprouter"
revision = "d1898390779332322e6b5ca5011da4bf249bb056"
# Prometheus are currently pre v1.0, so the API is changing considerably.
# The latest release (0.8) predates exported API requirements.
[[constraint]]
name = "github.com/prometheus/client_golang"
revision = "661e31bf844dfca9aeba15f27ea8aa0d485ad212"
[[constraint]]
name = "go.uber.org/zap"
version = "1.8.0"
[prune]
go-tests = true
unused-packages = true

View File

@ -8,7 +8,3 @@ This project is using [`dep`](https://golang.github.io/dep/docs/introduction.htm
Use `dep ensure -vendor-only` when you only need to populate the `vendor` directory to run `go build` successfully,
or run `dep ensure` to both populate the `vendor` directory and update `Gopkg.lock` with any newer constraints.
For more detail about our team's `dep` workflows, refer to our
[draft dep guide](https://docs.google.com/document/d/1a7VsgPBFmeRxVLOKz_NrGRlBI88tcWniZPPMJTu5ez0/edit)
(which [should eventually be merged into the `docs` folder](https://github.com/influxdata/idpe/issues/467)).

151
auth.go Normal file
View File

@ -0,0 +1,151 @@
package platform
import (
"context"
"errors"
"fmt"
"regexp"
)
// Authorization is a authorization. 🎉
type Authorization struct {
Token string `json:"token"`
UserID ID `json:"userID,omitempty"`
Permissions []Permission `json:"permissions"`
}
// AuthorizationService represents a service for managing authorization data.
type AuthorizationService interface {
// Returns a single authorization by Token.
FindAuthorizationByToken(ctx context.Context, t string) (*Authorization, error)
// Returns a list of authorizations that match filter and the total count of matching authorizations.
// Additional options provide pagination & sorting.
FindAuthorizations(ctx context.Context, filter AuthorizationFilter, opt ...FindOptions) ([]*Authorization, int, error)
// Creates a new authorization and sets a.Token with the new identifier.
CreateAuthorization(ctx context.Context, a *Authorization) error
// Removes a authorization by token.
DeleteAuthorization(ctx context.Context, t string) error
}
// AuthorizationFilter represents a set of filter that restrict the returned results.
type AuthorizationFilter struct {
Token *string
UserID *ID
}
type action string
const (
// ReadAction is the action for reading.
ReadAction action = "read"
// WriteAction is the action for writing.
WriteAction action = "write"
// CreateAction is the action for creating new resources.
CreateAction action = "create"
// DeleteAction is the action for deleting an existing resource.
DeleteAction action = "delete"
)
type resource string
const (
// UserResource represents the user resource actions can apply to.
UserResource = resource("user")
// OrganizationResource represents the org resource actions can apply to.
OrganizationResource = resource("org")
)
// BucketResource constructs a bucket resource.
func BucketResource(t string, b string) resource {
return resource(fmt.Sprintf("org:%s:bucket:%s", t, b))
}
// Permission defines an action and a resource.
type Permission struct {
Action action `json:"action"`
Resource resource `json:"resource"`
}
func (p Permission) String() string {
return fmt.Sprintf("%s:%s", p.Action, p.Resource)
}
// ConstructPermission constructs a permission from a provided action and resource.
func ConstructPermission(a string, r string) (Permission, error) {
constructedAction := action(a)
constructedResource := resource(r)
if !validAction(constructedAction) {
return Permission{}, errors.New("invalid permission action")
} else if !validResource(constructedResource) {
return Permission{}, errors.New("invalid permission resource")
}
p := Permission{
Action: constructedAction,
Resource: constructedResource,
}
return p, nil
}
func validAction(a action) bool {
validActions := [4]action{ReadAction, WriteAction, CreateAction, DeleteAction}
for _, x := range validActions {
if a == x {
return true
}
}
return false
}
func validResource(r resource) bool {
validResources := [2]resource{UserResource, OrganizationResource}
bucketRegex, _ := regexp.Compile(`org:.+:bucket:.+`)
for _, x := range validResources {
if r == x {
return true
}
}
return bucketRegex.MatchString(string(r))
}
var (
// CreateUser is a permission for creating users.
CreateUser = Permission{
Action: CreateAction,
Resource: UserResource,
}
// DeleteUser is a permission for deleting users.
DeleteUser = Permission{
Action: DeleteAction,
Resource: UserResource,
}
)
// ReadBucket constructs a permission for reading a bucket.
func ReadBucket(o, b string) Permission {
return Permission{
Action: ReadAction,
Resource: BucketResource(o, b),
}
}
// WriteBucket constructs a permission for writing to a bucket.
func WriteBucket(o, b string) Permission {
return Permission{
Action: WriteAction,
Resource: BucketResource(o, b),
}
}
// Allowed returns true if the permission exists in a list of permissions.
func Allowed(req Permission, ps []Permission) bool {
for _, p := range ps {
if p.Action == req.Action && p.Resource == req.Resource {
return true
}
}
return false
}

59
bucket.go Normal file
View File

@ -0,0 +1,59 @@
package platform
import (
"context"
"time"
)
// Bucket is a bucket. 🎉
type Bucket struct {
ID ID `json:"id"`
OrganizationID ID `json:"organizationID"`
Name string `json:"name"`
RetentionPeriod time.Duration `json:"retentionPeriod"`
}
// BucketService represents a service for managing bucket data.
type BucketService interface {
// FindBucketByID returns a single bucket by ID.
FindBucketByID(ctx context.Context, id ID) (*Bucket, error)
// FindBucket returns the first bucket that matches filter.
FindBucket(ctx context.Context, filter BucketFilter) (*Bucket, error)
// FindBuckets returns a list of buckets that match filter and the total count of matching buckets.
// Additional options provide pagination & sorting.
FindBuckets(ctx context.Context, filter BucketFilter, opt ...FindOptions) ([]*Bucket, int, error)
// CreateBucket creates a new bucket and sets b.ID with the new identifier.
CreateBucket(ctx context.Context, b *Bucket) error
// UpdateBucket updates a single bucket with changeset.
// Returns the new bucket state after update.
UpdateBucket(ctx context.Context, id ID, upd BucketUpdate) (*Bucket, error)
// DeleteBucket removes a bucket by ID.
DeleteBucket(ctx context.Context, id ID) error
}
// BucketUpdate represents updates to a bucket.
// Only fields which are set are updated.
type BucketUpdate struct {
Name *string `json:"name,omitempty"`
RetentionPeriod *time.Duration `json:"retentionPeriod,omitempty"`
}
// BucketFilter represents a set of filter that restrict the returned results.
type BucketFilter struct {
ID *ID
OrganizationID *ID
Name *string
}
// FindOptions represents options passed to all find methods with multiple results.
type FindOptions struct {
Limit int
Offset int
SortBy string
Descending bool
}

52
context/token.go Normal file
View File

@ -0,0 +1,52 @@
package context
import (
"context"
"net/http"
"github.com/influxdata/platform"
"github.com/influxdata/platform/kit/errors"
)
type contextKey string
const (
authorizationCtxKey = contextKey("influx/authorization/v1")
tokenCtxKey = contextKey("influx/token/v1")
)
// SetAuthorization sets an authorization on context.
func SetAuthorization(ctx context.Context, a *platform.Authorization) context.Context {
return context.WithValue(ctx, authorizationCtxKey, a)
}
// GetAuthorization retrieves an authorization from context.
func GetAuthorization(ctx context.Context) (*platform.Authorization, error) {
a, ok := ctx.Value(authorizationCtxKey).(*platform.Authorization)
if !ok {
return nil, errors.InternalErrorf("authorization not found on context")
}
return a, nil
}
// SetToken sets an authorization on context.
func SetToken(ctx context.Context, t string) context.Context {
return context.WithValue(ctx, tokenCtxKey, t)
}
// GeToken retrieves an authorization from context.
func GetToken(ctx context.Context) (string, error) {
t, ok := ctx.Value(tokenCtxKey).(string)
if !ok {
return "", errors.InternalErrorf("token not found on context")
}
return t, nil
}
// TokenFromAuthorizationHeader retrieves a auth token from the HTTP Authorization header.
var TokenFromAuthorizationHeader = func(ctx context.Context, r *http.Request) context.Context {
token := r.Header.Get("Authorization")
return SetToken(ctx, token)
}

89
http/README.md Normal file
View File

@ -0,0 +1,89 @@
# HTTP Handler Style Guide
### HTTP Handler
* Each handler should implement `http.Handler`
- This can be done by embedding a [`httprouter.Router`](https://github.com/julienschmidt/httprouter)
(a light weight HTTP router that supports variables in the routing pattern and matches against the request method)
* Required services should be exported on the struct
```go
// ThingHandler represents an HTTP API handler for things.
type ThingHandler struct {
// embedded httprouter.Router as a lazy way to implement http.Handler
*httprouter.Router
ThingService platform.ThingService
AuthorizationService platform.AuthorizationService
Logger *zap.Logger
}
```
### HTTP Handler Constructor
* Routes should be declared in the constructor
```go
// NewThingHandler returns a new instance of ThingHandler.
func NewThingHandler() *ThingHandler {
h := &ThingHandler{
Router: httprouter.New(),
Logger: zap.Nop(),
}
h.HandlerFunc("POST", "/v1/things", h.handlePostThing)
h.HandlerFunc("GET", "/v1/things", h.handleGetThings)
return h
}
```
### Route handlers (`http.HandlerFunc`s)
* Each route handler should have an associated request struct and decode function
* The decode function should take a `context.Context` and an `*http.Request` and return the associated route request struct
```go
type postThingRequest struct {
Thing *platform.Thing
}
func decodePostThingRequest(ctx context.Context, r *http.Request) (*postThingRequest, error) {
t := &platform.Thing{}
if err := json.NewDecoder(r.Body).Decode(t); err != nil {
return nil, err
}
return &postThingRequest{
Thing: t,
}, nil
}
```
* Route `http.HandlerFuncs` should separate the decoding and encoding of HTTP requests/response from actual handler logic
```go
// handlePostThing is the HTTP handler for the POST /v1/things route.
func (h *ThingHandler) handlePostThing(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostThingRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
// Do stuff here
if err := h.ThingService.CreateThing(ctx, req.Thing); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, req.Thing); err != nil {
h.Logger.Info("encoding response failed", zap.Error(err))
return
}
}
```
* `http.HandlerFunc`'s that require particular encoding of http responses should implement an encode response function

313
http/auth_service.go Normal file
View File

@ -0,0 +1,313 @@
package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"go.uber.org/zap"
"github.com/influxdata/platform"
"github.com/influxdata/platform/kit/errors"
"github.com/julienschmidt/httprouter"
)
// AuthorizationHandler represents an HTTP API handler for authorizations.
type AuthorizationHandler struct {
*httprouter.Router
Logger *zap.Logger
AuthorizationService platform.AuthorizationService
}
// NewAuthorizationHandler returns a new instance of AuthorizationHandler.
func NewAuthorizationHandler() *AuthorizationHandler {
h := &AuthorizationHandler{
Router: httprouter.New(),
}
h.HandlerFunc("POST", "/v1/authorizations", h.handlePostAuthorization)
h.HandlerFunc("GET", "/v1/authorizations", h.handleGetAuthorizations)
// TODO(desa): Remove this when we get the all clear
h.HandlerFunc("POST", "/influx/v1/authorizations", h.handlePostAuthorization)
h.HandlerFunc("GET", "/influx/v1/authorizations", h.handleGetAuthorizations)
return h
}
// handlePostAuthorization is the HTTP handler for the POST /v1/authorizations route.
func (h *AuthorizationHandler) handlePostAuthorization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostAuthorizationRequest(ctx, r)
if err != nil {
h.Logger.Info("failed to decode request", zap.String("handler", "postAuthorization"), zap.Error(err))
errors.EncodeHTTP(ctx, err, w)
return
}
// TODO: Need to do some validation of req.Authorization.Permissions
if err := h.AuthorizationService.CreateAuthorization(ctx, req.Authorization); err != nil {
// Don't log here, it should already be handled by the service
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, req.Authorization); err != nil {
h.Logger.Info("failed to encode response", zap.String("handler", "postAuthorization"), zap.Error(err))
errors.EncodeHTTP(ctx, err, w)
return
}
}
type postAuthorizationRequest struct {
Authorization *platform.Authorization
}
func decodePostAuthorizationRequest(ctx context.Context, r *http.Request) (*postAuthorizationRequest, error) {
a := &platform.Authorization{}
if err := json.NewDecoder(r.Body).Decode(a); err != nil {
return nil, err
}
return &postAuthorizationRequest{
Authorization: a,
}, nil
}
// handleGetAuthorizations is the HTTP handler for the GET /v1/authorizations route.
func (h *AuthorizationHandler) handleGetAuthorizations(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetAuthorizationsRequest(ctx, r)
if err != nil {
h.Logger.Info("failed to decode request", zap.String("handler", "getAuthorizations"), zap.Error(err))
errors.EncodeHTTP(ctx, err, w)
return
}
as, _, err := h.AuthorizationService.FindAuthorizations(ctx, req.filter)
if err != nil {
// Don't log here, it should already be handled by the service
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, as); err != nil {
h.Logger.Info("failed to encode response", zap.String("handler", "getAuthorizations"), zap.Error(err))
errors.EncodeHTTP(ctx, err, w)
return
}
}
type getAuthorizationsRequest struct {
filter platform.AuthorizationFilter
}
func decodeGetAuthorizationsRequest(ctx context.Context, r *http.Request) (*getAuthorizationsRequest, error) {
qp := r.URL.Query()
userID := qp.Get("userID")
req := &getAuthorizationsRequest{}
if userID != "" {
var id platform.ID
if err := (&id).Decode([]byte(userID)); err != nil {
return nil, err
}
req.filter.UserID = &id
}
return req, nil
}
// AuthorizationService connects to Influx via HTTP using tokens to manage authorizations
type AuthorizationService struct {
Addr string
Token string
InsecureSkipVerify bool
}
var _ platform.AuthorizationService = (*AuthorizationService)(nil)
// FindAuthorizationByToken returns a single authorization by Token.
func (s *AuthorizationService) FindAuthorizationByToken(ctx context.Context, token string) (*platform.Authorization, error) {
u, err := newURL(s.Addr, authorizationTokenPath(token))
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", s.Token)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if resp.StatusCode != http.StatusNoContent {
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return nil, err
}
return nil, reqErr
}
var b platform.Authorization
if err := dec.Decode(&b); err != nil {
return nil, err
}
return &b, nil
}
// FindAuthorization returns the first authorization that matches filter.
func (s *AuthorizationService) FindAuthorization(ctx context.Context, filter platform.AuthorizationFilter) (*platform.Authorization, error) {
authorizations, n, err := s.FindAuthorizations(ctx, filter)
if err != nil {
return nil, err
}
if n == 0 {
return nil, fmt.Errorf("found no matching authorization")
}
return authorizations[0], nil
}
// FindAuthorizations returns a list of authorizations that match filter and the total count of matching authorizations.
// Additional options provide pagination & sorting.
func (s *AuthorizationService) FindAuthorizations(ctx context.Context, filter platform.AuthorizationFilter, opt ...platform.FindOptions) ([]*platform.Authorization, int, error) {
u, err := newURL(s.Addr, authorizationPath)
if err != nil {
return nil, 0, err
}
query := u.Query()
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, 0, err
}
req.URL.RawQuery = query.Encode()
req.Header.Set("Authorization", s.Token)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if resp.StatusCode != http.StatusOK {
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return nil, 0, err
}
return nil, 0, reqErr
}
var bs []*platform.Authorization
if err := dec.Decode(&bs); err != nil {
return nil, 0, err
}
return bs, len(bs), nil
}
const (
authorizationPath = "/v1/authorizations"
)
// CreateAuthorization creates a new authorization and sets b.ID with the new identifier.
func (s *AuthorizationService) CreateAuthorization(ctx context.Context, b *platform.Authorization) error {
u, err := newURL(s.Addr, authorizationPath)
if err != nil {
return err
}
octets, err := json.Marshal(b)
if err != nil {
return err
}
req, err := http.NewRequest("POST", u.String(), bytes.NewReader(octets))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", s.Token)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
// TODO: this should really check the error from the headers
if resp.StatusCode != http.StatusCreated {
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return err
}
return reqErr
}
if err := json.NewDecoder(resp.Body).Decode(b); err != nil {
return err
}
return nil
}
// DeleteAuthorization removes a authorization by token.
func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, token string) error {
u, err := newURL(s.Addr, authorizationTokenPath(token))
if err != nil {
return err
}
req, err := http.NewRequest("DELETE", u.String(), nil)
if err != nil {
return err
}
req.Header.Set("Authorization", s.Token)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return err
}
return reqErr
}
return nil
}
func authorizationTokenPath(token string) string {
return authorizationPath + "/" + token
}

455
http/bucket_service.go Normal file
View File

@ -0,0 +1,455 @@
package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/influxdata/platform"
"github.com/influxdata/platform/kit/errors"
"github.com/julienschmidt/httprouter"
)
// BucketHandler represents an HTTP API handler for buckets.
type BucketHandler struct {
*httprouter.Router
BucketService platform.BucketService
}
// NewBucketHandler returns a new instance of BucketHandler.
func NewBucketHandler() *BucketHandler {
h := &BucketHandler{
Router: httprouter.New(),
}
h.HandlerFunc("POST", "/v1/buckets", h.handlePostBucket)
h.HandlerFunc("GET", "/v1/buckets", h.handleGetBuckets)
h.HandlerFunc("GET", "/v1/buckets/:id", h.handleGetBucket)
h.HandlerFunc("PATCH", "/v1/buckets/:id", h.handlePatchBucket)
return h
}
// handlePostBucket is the HTTP handler for the POST /v1/buckets route.
func (h *BucketHandler) handlePostBucket(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostBucketRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := h.BucketService.CreateBucket(ctx, req.Bucket); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, req.Bucket); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type postBucketRequest struct {
Bucket *platform.Bucket
}
func decodePostBucketRequest(ctx context.Context, r *http.Request) (*postBucketRequest, error) {
b := &platform.Bucket{}
if err := json.NewDecoder(r.Body).Decode(b); err != nil {
return nil, err
}
return &postBucketRequest{
Bucket: b,
}, nil
}
// handleGetBucket is the HTTP handler for the GET /v1/buckets/:id route.
func (h *BucketHandler) handleGetBucket(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetBucketRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
b, err := h.BucketService.FindBucketByID(ctx, req.BucketID)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type getBucketRequest struct {
BucketID platform.ID
}
func decodeGetBucketRequest(ctx context.Context, r *http.Request) (*getBucketRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
if err := (&i).Decode([]byte(id)); err != nil {
return nil, err
}
req := &getBucketRequest{
BucketID: i,
}
return req, nil
}
// handleGetBuckets is the HTTP handler for the GET /v1/buckets route.
func (h *BucketHandler) handleGetBuckets(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetBucketsRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
bs, _, err := h.BucketService.FindBuckets(ctx, req.filter)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, bs); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type getBucketsRequest struct {
filter platform.BucketFilter
}
func decodeGetBucketsRequest(ctx context.Context, r *http.Request) (*getBucketsRequest, error) {
qp := r.URL.Query()
req := &getBucketsRequest{}
if id := qp.Get("orgID"); id != "" {
req.filter.OrganizationID = &platform.ID{}
if err := req.filter.OrganizationID.DecodeFromString(id); err != nil {
return nil, err
}
}
if id := qp.Get("bucketID"); id != "" {
req.filter.ID = &platform.ID{}
if err := req.filter.ID.DecodeFromString(id); err != nil {
return nil, err
}
}
if name := qp.Get("bucketName"); name != "" {
req.filter.Name = &name
}
return req, nil
}
// handlePatchBucket is the HTTP handler for the PATH /v1/buckets route.
func (h *BucketHandler) handlePatchBucket(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePatchBucketRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
b, err := h.BucketService.UpdateBucket(ctx, req.BucketID, req.Update)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type patchBucketRequest struct {
Update platform.BucketUpdate
BucketID platform.ID
}
func decodePatchBucketRequest(ctx context.Context, r *http.Request) (*patchBucketRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
if err := (&i).Decode([]byte(id)); err != nil {
return nil, err
}
var upd platform.BucketUpdate
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
return nil, err
}
return &patchBucketRequest{
Update: upd,
BucketID: i,
}, nil
}
const (
bucketPath = "/v1/buckets"
)
// BucketService connects to Influx via HTTP using tokens to manage buckets
type BucketService struct {
Addr string
Token string
InsecureSkipVerify bool
}
// FindBucketByID returns a single bucket by ID.
func (s *BucketService) FindBucketByID(ctx context.Context, id platform.ID) (*platform.Bucket, error) {
u, err := newURL(s.Addr, bucketplatformath(id))
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", s.Token)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if resp.StatusCode != http.StatusNoContent {
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return nil, err
}
return nil, reqErr
}
var b platform.Bucket
if err := dec.Decode(&b); err != nil {
return nil, err
}
return &b, nil
}
// FindBucket returns the first bucket that matches filter.
func (s *BucketService) FindBucket(ctx context.Context, filter platform.BucketFilter) (*platform.Bucket, error) {
bs, n, err := s.FindBuckets(ctx, filter)
if err != nil {
return nil, err
}
if n == 0 {
return nil, fmt.Errorf("found no matching buckets")
}
return bs[0], nil
}
// FindBuckets returns a list of buckets that match filter and the total count of matching buckets.
// Additional options provide pagination & sorting.
func (s *BucketService) FindBuckets(ctx context.Context, filter platform.BucketFilter, opt ...platform.FindOptions) ([]*platform.Bucket, int, error) {
u, err := newURL(s.Addr, bucketPath)
if err != nil {
return nil, 0, err
}
query := u.Query()
if filter.OrganizationID != nil {
query.Add("orgID", filter.OrganizationID.String())
}
if filter.ID != nil {
query.Add("bucketID", filter.ID.String())
}
if filter.Name != nil {
query.Add("bucketName", *filter.Name)
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, 0, err
}
req.URL.RawQuery = query.Encode()
req.Header.Set("Authorization", s.Token)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if resp.StatusCode != http.StatusOK {
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return nil, 0, err
}
return nil, 0, reqErr
}
var bs []*platform.Bucket
if err := dec.Decode(&bs); err != nil {
return nil, 0, err
}
return bs, len(bs), nil
}
// CreateBucket creates a new bucket and sets b.ID with the new identifier.
func (s *BucketService) CreateBucket(ctx context.Context, b *platform.Bucket) error {
u, err := newURL(s.Addr, bucketPath)
if err != nil {
return err
}
octets, err := json.Marshal(b)
if err != nil {
return err
}
req, err := http.NewRequest("POST", u.String(), bytes.NewReader(octets))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", s.Token)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
// TODO: this should really check the error from the headers
if resp.StatusCode != http.StatusCreated {
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return err
}
return reqErr
}
if err := json.NewDecoder(resp.Body).Decode(b); err != nil {
return err
}
return nil
}
// UpdateBucket updates a single bucket with changeset.
// Returns the new bucket state after update.
func (s *BucketService) UpdateBucket(ctx context.Context, id platform.ID, upd platform.BucketUpdate) (*platform.Bucket, error) {
u, err := newURL(s.Addr, bucketplatformath(id))
if err != nil {
return nil, err
}
octets, err := json.Marshal(upd)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PATCH", u.String(), bytes.NewReader(octets))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", s.Token)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if resp.StatusCode != http.StatusCreated {
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return nil, err
}
return nil, reqErr
}
var b platform.Bucket
if err := dec.Decode(&b); err != nil {
return nil, err
}
return &b, nil
}
// DeleteBucket removes a bucket by ID.
func (s *BucketService) DeleteBucket(ctx context.Context, id platform.ID) error {
u, err := newURL(s.Addr, bucketplatformath(id))
if err != nil {
return err
}
req, err := http.NewRequest("DELETE", u.String(), nil)
if err != nil {
return err
}
req.Header.Set("Authorization", s.Token)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return err
}
return reqErr
}
return nil
}
func bucketplatformath(id platform.ID) string {
return bucketPath + "/" + id.String()
}

35
http/client.go Normal file
View File

@ -0,0 +1,35 @@
package http
import (
"crypto/tls"
"net/http"
"net/url"
)
// Shared transports for all clients to prevent leaking connections
var (
skipVerifyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
defaultTransport = &http.Transport{}
)
func newURL(addr, path string) (*url.URL, error) {
u, err := url.Parse(addr)
if err != nil {
return nil, err
}
u.Path = path
return u, nil
}
func newClient(scheme string, insecure bool) *http.Client {
hc := &http.Client{
Transport: defaultTransport,
}
if scheme == "https" && insecure {
hc.Transport = skipVerifyTransport
}
return hc
}

116
http/handler.go Normal file
View File

@ -0,0 +1,116 @@
package http
import (
"context"
"encoding/json"
"net/http"
_ "net/http/pprof"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
MetricsPath = "/metrics"
HealthzPath = "/healthz"
DebugPath = "/debug"
)
// Handler provides basic handling of metrics, healthz and debug endpoints.
// All other requests are passed down to the sub handler.
type Handler struct {
name string
// HealthzHandler handles healthz requests
HealthzHandler http.Handler
// MetricsHandler handles metrics requests
MetricsHandler http.Handler
// DebugHandler handles debug requests
DebugHandler http.Handler
// Handler handles all other requests
Handler http.Handler
}
// NewHandler creates a new handler with the given name.
// The name is used to tag the metrics produced by this handler.
func NewHandler(name string) *Handler {
return &Handler{
name: name,
MetricsHandler: promhttp.Handler(),
DebugHandler: http.DefaultServeMux,
}
}
// ServeHTTP delegates a request to the appropriate subhandler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// TODO: better way to do this?
statusW := newStatusResponseWriter(w)
w = statusW
// TODO: This could be problematic eventually. But for now it should be fine.
defer func() {
httpRequests.With(prometheus.Labels{
"handler": h.name,
"method": r.Method,
"path": r.URL.Path,
"status": statusW.statusCodeClass(),
}).Inc()
}()
defer func(start time.Time) {
httpRequestDuration.With(prometheus.Labels{
"handler": h.name,
"method": r.Method,
"path": r.URL.Path,
"status": statusW.statusCodeClass(),
}).Observe(time.Since(start).Seconds())
}(time.Now())
switch {
case r.URL.Path == MetricsPath:
h.MetricsHandler.ServeHTTP(w, r)
case r.URL.Path == HealthzPath:
h.HealthzHandler.ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, DebugPath):
h.DebugHandler.ServeHTTP(w, r)
default:
h.Handler.ServeHTTP(w, r)
}
}
func encodeResponse(ctx context.Context, w http.ResponseWriter, code int, res interface{}) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
return json.NewEncoder(w).Encode(res)
}
func init() {
prometheus.MustRegister(httpCollectors...)
}
// namespace is the leading part of all published metrics for the http handler service.
const namespace = "http"
const handlerSubsystem = "api"
// http metrics track request latency and count by path.
var (
httpRequests = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: handlerSubsystem,
Name: "requests_total",
Help: "Number of http requests received",
}, []string{"handler", "method", "path", "status"})
httpRequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: handlerSubsystem,
Name: "request_duration_seconds",
Help: "Time taken to respond to HTTP request",
// TODO(desa): determine what spacing these buckets should have.
Buckets: prometheus.ExponentialBuckets(0.001, 1.5, 25),
}, []string{"handler", "method", "path", "status"})
httpCollectors = []prometheus.Collector{httpRequests, httpRequestDuration}
)

339
http/org_service.go Normal file
View File

@ -0,0 +1,339 @@
package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/influxdata/platform"
"github.com/influxdata/platform/kit/errors"
"github.com/julienschmidt/httprouter"
)
// OrgHandler represents an HTTP API handler for orgs.
type OrgHandler struct {
*httprouter.Router
OrganizationService platform.OrganizationService
}
// NewOrgHandler returns a new instance of OrgHandler.
func NewOrgHandler() *OrgHandler {
h := &OrgHandler{
Router: httprouter.New(),
}
h.HandlerFunc("POST", "/v1/orgs", h.handlePostOrg)
h.HandlerFunc("GET", "/v1/orgs", h.handleGetOrgs)
h.HandlerFunc("GET", "/v1/orgs/:id", h.handleGetOrg)
h.HandlerFunc("PATCH", "/v1/orgs/:id", h.handlePatchOrg)
return h
}
// handlePostOrg is the HTTP handler for the POST /v1/orgs route.
func (h *OrgHandler) handlePostOrg(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostOrgRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := h.OrganizationService.CreateOrganization(ctx, req.Org); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, req.Org); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type postOrgRequest struct {
Org *platform.Organization
}
func decodePostOrgRequest(ctx context.Context, r *http.Request) (*postOrgRequest, error) {
o := &platform.Organization{}
if err := json.NewDecoder(r.Body).Decode(o); err != nil {
return nil, err
}
return &postOrgRequest{
Org: o,
}, nil
}
// handleGetOrg is the HTTP handler for the GET /v1/orgs/:id route.
func (h *OrgHandler) handleGetOrg(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetOrgRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
b, err := h.OrganizationService.FindOrganizationByID(ctx, req.OrgID)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type getOrgRequest struct {
OrgID platform.ID
}
func decodeGetOrgRequest(ctx context.Context, r *http.Request) (*getOrgRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
if err := (&i).Decode([]byte(id)); err != nil {
return nil, err
}
req := &getOrgRequest{
OrgID: i,
}
return req, nil
}
// handleGetOrgs is the HTTP handler for the GET /v1/orgs route.
func (h *OrgHandler) handleGetOrgs(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetOrgsRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
orgs, _, err := h.OrganizationService.FindOrganizations(ctx, req.filter)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, orgs); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type getOrgsRequest struct {
filter platform.OrganizationFilter
}
func decodeGetOrgsRequest(ctx context.Context, r *http.Request) (*getOrgsRequest, error) {
qp := r.URL.Query()
req := &getOrgsRequest{}
if id := qp.Get("orgID"); id != "" {
req.filter.ID = &platform.ID{}
if err := req.filter.ID.DecodeFromString(id); err != nil {
return nil, err
}
}
if name := qp.Get("orgName"); name != "" {
req.filter.Name = &name
}
return req, nil
}
// handlePatchOrg is the HTTP handler for the PATH /v1/orgs route.
func (h *OrgHandler) handlePatchOrg(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePatchOrgRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
o, err := h.OrganizationService.UpdateOrganization(ctx, req.OrgID, req.Update)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, o); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type patchOrgRequest struct {
Update platform.OrganizationUpdate
OrgID platform.ID
}
func decodePatchOrgRequest(ctx context.Context, r *http.Request) (*patchOrgRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
if err := (&i).Decode([]byte(id)); err != nil {
return nil, err
}
var upd platform.OrganizationUpdate
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
return nil, err
}
return &patchOrgRequest{
Update: upd,
OrgID: i,
}, nil
}
const (
organizationPath = "/v1/orgs"
)
// OrganizationService connects to Influx via HTTP using tokens to manage organizations.
type OrganizationService struct {
Addr string
Token string
InsecureSkipVerify bool
}
func (s *OrganizationService) FindOrganizationByID(ctx context.Context, id platform.ID) (*platform.Organization, error) {
filter := platform.OrganizationFilter{ID: &id}
return s.FindOrganization(ctx, filter)
}
func (s *OrganizationService) FindOrganization(ctx context.Context, filter platform.OrganizationFilter) (*platform.Organization, error) {
os, n, err := s.FindOrganizations(ctx, filter)
if err != nil {
return nil, err
}
if n < 1 {
return nil, fmt.Errorf("expected at least one organization")
}
return os[0], nil
}
func (s *OrganizationService) FindOrganizations(ctx context.Context, filter platform.OrganizationFilter, opt ...platform.FindOptions) ([]*platform.Organization, int, error) {
url, err := newURL(s.Addr, organizationPath)
if err != nil {
return nil, 0, err
}
qp := url.Query()
if filter.Name != nil {
qp.Add("orgName", *filter.Name)
}
if filter.ID != nil {
qp.Add("orgID", filter.ID.String())
}
url.RawQuery = qp.Encode()
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, 0, err
}
req.Header.Set("Authorization", s.Token)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, 0, err
}
// TODO: this should really check the error from the headers
if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return nil, 0, err
}
return nil, 0, reqErr
}
var os []*platform.Organization
if err := json.NewDecoder(resp.Body).Decode(&os); err != nil {
return nil, 0, err
}
return os, len(os), nil
}
// CreateOrganization creates an organization.
func (s *OrganizationService) CreateOrganization(ctx context.Context, o *platform.Organization) error {
url, err := newURL(s.Addr, organizationPath)
if err != nil {
return err
}
octets, err := json.Marshal(o)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url.String(), bytes.NewReader(octets))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", s.Token)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
// TODO: this should really check the error from the headers
if resp.StatusCode != http.StatusCreated {
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return err
}
return reqErr
}
if err := json.NewDecoder(resp.Body).Decode(o); err != nil {
return err
}
return nil
}
func (s *OrganizationService) UpdateOrganization(ctx context.Context, id platform.ID, upd platform.OrganizationUpdate) (*platform.Organization, error) {
panic("not implemented")
}
func (s *OrganizationService) DeleteOrganization(ctx context.Context, id platform.ID) error {
panic("not implemented")
}

View File

@ -0,0 +1,37 @@
package http
import "net/http"
type statusResponseWriter struct {
statusCode int
http.ResponseWriter
}
func newStatusResponseWriter(w http.ResponseWriter) *statusResponseWriter {
return &statusResponseWriter{
ResponseWriter: w,
}
}
// WriteHeader writes
func (w *statusResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *statusResponseWriter) statusCodeClass() string {
class := "XXX"
switch w.statusCode / 100 {
case 1:
class = "1XX"
case 2:
class = "2XX"
case 3:
class = "3XX"
case 4:
class = "4XX"
case 5:
class = "5XX"
}
return class
}

348
http/swagger.yml Normal file
View File

@ -0,0 +1,348 @@
openapi: "3.0.0"
info:
title: Gateway Service
version: 0.1.0
servers:
- url: /v1
paths:
/buckets:
get:
tags:
- Buckets
summary: List all buckets
responses:
'200':
description: a list of buckets
content:
application/json:
schema:
$ref: "#/components/schemas/Buckets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
tags:
- Buckets
summary: Create a bucket
requestBody:
description: bucket to create
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Bucket"
responses:
'201':
description: Bucket created
content:
application/json:
schema:
$ref: "#/components/schemas/Bucket"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/buckets/{bucketId}':
get:
tags:
- Buckets
summary: Retrieve a bucket
parameters:
- in: path
name: bucketId
schema:
type: string
required: true
description: ID of bucket to get
responses:
'200':
description: bucket details
content:
application/json:
schema:
$ref: "#/components/schemas/Bucket"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Buckets
summary: Update a bucket
requestBody:
description: bucket update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Bucket"
parameters:
- in: path
name: bucketId
schema:
type: string
required: true
description: ID of bucket to update
responses:
'200':
description: An updated bucket
content:
application/json:
schema:
$ref: "#/components/schemas/Bucket"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/orgs:
get:
tags:
- Organizations
summary: List all organizations
responses:
'200':
description: A list of organizations
content:
application/json:
schema:
$ref: "#/components/schemas/Organizations"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
tags:
- Organizations
summary: Create an organization
requestBody:
description: organization to create
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Organization"
responses:
'201':
description: organization created
content:
application/json:
schema:
$ref: "#/components/schemas/Organization"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/orgs/{orgId}':
get:
tags:
- Organizations
summary: Retrieve an organization
parameters:
- in: path
name: orgId
schema:
type: string
required: true
description: ID of organization to get
responses:
'200':
description: organization details
content:
application/json:
schema:
$ref: "#/components/schemas/Organization"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Organizations
summary: Update an organization
requestBody:
description: organization update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Organization"
parameters:
- in: path
name: orgId
schema:
type: string
required: true
description: ID of organization to get
responses:
'200':
description: organization updated
content:
application/json:
schema:
$ref: "#/components/schemas/Organization"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/users:
get:
tags:
- Users
summary: List all users
responses:
'200':
description: a list of users
content:
application/json:
schema:
$ref: "#/components/schemas/Users"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
tags:
- Users
summary: Create a user
requestBody:
description: user to create
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/User"
responses:
'201':
description: user created
content:
application/json:
schema:
$ref: "#/components/schemas/User"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/users/{userId}':
get:
tags:
- Users
summary: Retrieve a user
parameters:
- in: path
name: userId
schema:
type: string
required: true
description: ID of user to get
responses:
'200':
description: user details
content:
application/json:
schema:
$ref: "#/components/schemas/User"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Users
summary: Update a user
requestBody:
description: user update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/User"
parameters:
- in: path
name: userId
schema:
type: string
required: true
description: ID of user to update
responses:
'200':
description: user updated
content:
application/json:
schema:
$ref: "#/components/schemas/User"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Bucket:
properties:
id:
type: string
organizationId:
type: string
name:
type: string
retentionPeriod:
type: integer
format: int64
Buckets:
type: array
items:
$ref: "#/components/schemas/Bucket"
Organization:
properties:
id:
type: string
name:
type: string
Organizations:
type: array
items:
$ref: "#/components/schemas/Organization"
User:
properties:
id:
type: string
name:
type: string
Users:
type: array
items:
$ref: "#/components/schemas/User"
Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

126
http/usage_service.go Normal file
View File

@ -0,0 +1,126 @@
package http
import (
"context"
"fmt"
"net/http"
"time"
"github.com/influxdata/platform"
"github.com/influxdata/platform/kit/errors"
"github.com/julienschmidt/httprouter"
)
// UsageHandler represents an HTTP API handler for usages.
type UsageHandler struct {
*httprouter.Router
UsageService platform.UsageService
}
// NewUsageHandler returns a new instance of UsageHandler.
func NewUsageHandler() *UsageHandler {
h := &UsageHandler{
Router: httprouter.New(),
}
h.HandlerFunc("GET", "/v1/usage", h.handleGetUsage)
return h
}
// handleGetUsage is the HTTP handler for the GET /v1/usage route.
func (h *UsageHandler) handleGetUsage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetUsageRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
b, err := h.UsageService.GetUsage(ctx, req.filter)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type getUsageRequest struct {
filter platform.UsageFilter
}
func decodeGetUsageRequest(ctx context.Context, r *http.Request) (*getUsageRequest, error) {
req := &getUsageRequest{}
qp := r.URL.Query()
orgID := qp.Get("orgID")
if orgID != "" {
var id platform.ID
if err := (&id).DecodeFromString(orgID); err != nil {
return nil, err
}
req.filter.OrgID = &id
}
bucketID := qp.Get("bucketID")
if bucketID != "" {
var id platform.ID
if err := (&id).DecodeFromString(bucketID); err != nil {
return nil, err
}
req.filter.BucketID = &id
}
start := qp.Get("start")
stop := qp.Get("stop")
if start == "" && stop != "" {
return nil, fmt.Errorf("start query param required")
}
if start == "" && stop != "" {
return nil, fmt.Errorf("stop query param required")
}
if start == "" && stop == "" {
now := time.Now()
month := roundToMonth(now)
req.filter.Range = &platform.Timespan{
Start: month,
Stop: now,
}
}
if start != "" && stop != "" {
startTime, err := time.Parse(time.RFC3339, start)
if err != nil {
return nil, err
}
stopTime, err := time.Parse(time.RFC3339, start)
if err != nil {
return nil, err
}
req.filter.Range = &platform.Timespan{
Start: startTime,
Stop: stopTime,
}
}
return req, nil
}
func roundToMonth(t time.Time) time.Time {
h, m, s := t.Clock()
d := t.Day()
delta := (time.Duration(d) * 24 * time.Hour) + time.Duration(h)*time.Hour + time.Duration(m)*time.Minute + time.Duration(s)*time.Second
return t.Add(-1 * delta).Round(time.Minute)
}

413
http/user_service.go Normal file
View File

@ -0,0 +1,413 @@
package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/influxdata/platform"
"github.com/influxdata/platform/kit/errors"
"github.com/julienschmidt/httprouter"
)
// UserHandler represents an HTTP API handler for users.
type UserHandler struct {
*httprouter.Router
UserService platform.UserService
}
// NewUserHandler returns a new instance of UserHandler.
func NewUserHandler() *UserHandler {
h := &UserHandler{
Router: httprouter.New(),
}
h.HandlerFunc("POST", "/v1/users", h.handlePostUser)
h.HandlerFunc("GET", "/v1/users", h.handleGetUsers)
h.HandlerFunc("GET", "/v1/users/:id", h.handleGetUser)
h.HandlerFunc("PATCH", "/v1/users/:id", h.handlePatchUser)
return h
}
// handlePostUser is the HTTP handler for the POST /v1/users route.
func (h *UserHandler) handlePostUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostUserRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := h.UserService.CreateUser(ctx, req.User); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, req.User); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type postUserRequest struct {
User *platform.User
}
func decodePostUserRequest(ctx context.Context, r *http.Request) (*postUserRequest, error) {
b := &platform.User{}
if err := json.NewDecoder(r.Body).Decode(b); err != nil {
return nil, err
}
return &postUserRequest{
User: b,
}, nil
}
// handleGetUser is the HTTP handler for the GET /v1/users/:id route.
func (h *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetUserRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
b, err := h.UserService.FindUserByID(ctx, req.UserID)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type getUserRequest struct {
UserID platform.ID
}
func decodeGetUserRequest(ctx context.Context, r *http.Request) (*getUserRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
if err := (&i).Decode([]byte(id)); err != nil {
return nil, err
}
req := &getUserRequest{
UserID: i,
}
return req, nil
}
// handleGetUsers is the HTTP handler for the GET /v1/users route.
func (h *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
f := platform.UserFilter{}
users, _, err := h.UserService.FindUsers(ctx, f)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, users); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
// handlePatchUser is the HTTP handler for the PATH /v1/users route.
func (h *UserHandler) handlePatchUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePatchUserRequest(ctx, r)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
b, err := h.UserService.UpdateUser(ctx, req.UserID, req.Update)
if err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil {
errors.EncodeHTTP(ctx, err, w)
return
}
}
type patchUserRequest struct {
Update platform.UserUpdate
UserID platform.ID
}
func decodePatchUserRequest(ctx context.Context, r *http.Request) (*patchUserRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
if err := (&i).Decode([]byte(id)); err != nil {
return nil, err
}
var upd platform.UserUpdate
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
return nil, err
}
return &patchUserRequest{
Update: upd,
UserID: i,
}, nil
}
// UserService connects to Influx via HTTP using tokens to manage users
type UserService struct {
Addr string
Token string
InsecureSkipVerify bool
}
// FindUserByID returns a single user by ID.
func (s *UserService) FindUserByID(ctx context.Context, id platform.ID) (*platform.User, error) {
url, err := newURL(s.Addr, userIDPath(id))
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", s.Token)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if resp.StatusCode != http.StatusNoContent {
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return nil, err
}
return nil, reqErr
}
var b platform.User
if err := dec.Decode(&b); err != nil {
return nil, err
}
return &b, nil
}
// FindUser returns the first user that matches filter.
func (s *UserService) FindUser(ctx context.Context, filter platform.UserFilter) (*platform.User, error) {
users, n, err := s.FindUsers(ctx, filter)
if err != nil {
return nil, err
}
if n == 0 {
return nil, fmt.Errorf("found no matching user")
}
return users[0], nil
}
// FindUsers returns a list of users that match filter and the total count of matching users.
// Additional options provide pagination & sorting.
func (s *UserService) FindUsers(ctx context.Context, filter platform.UserFilter, opt ...platform.FindOptions) ([]*platform.User, int, error) {
url, err := newURL(s.Addr, userPath)
if err != nil {
return nil, 0, err
}
query := url.Query()
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, 0, err
}
req.URL.RawQuery = query.Encode()
req.Header.Set("Authorization", s.Token)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if resp.StatusCode != http.StatusOK {
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return nil, 0, err
}
return nil, 0, reqErr
}
var bs []*platform.User
if err := dec.Decode(&bs); err != nil {
return nil, 0, err
}
return bs, len(bs), nil
}
const (
userPath = "/v1/users"
)
// CreateUser creates a new user and sets u.ID with the new identifier.
func (s *UserService) CreateUser(ctx context.Context, u *platform.User) error {
url, err := newURL(s.Addr, userPath)
if err != nil {
return err
}
octets, err := json.Marshal(u)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url.String(), bytes.NewReader(octets))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", s.Token)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
// TODO: this should really check the error from the headers
if resp.StatusCode != http.StatusCreated {
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return err
}
return reqErr
}
if err := json.NewDecoder(resp.Body).Decode(u); err != nil {
return err
}
return nil
}
// UpdateUser updates a single user with changeset.
// Returns the new user state after update.
func (s *UserService) UpdateUser(ctx context.Context, id platform.ID, upd platform.UserUpdate) (*platform.User, error) {
url, err := newURL(s.Addr, userIDPath(id))
if err != nil {
return nil, err
}
octets, err := json.Marshal(upd)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PATCH", url.String(), bytes.NewReader(octets))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", s.Token)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
if resp.StatusCode != http.StatusCreated {
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return nil, err
}
return nil, reqErr
}
var u platform.User
if err := dec.Decode(&u); err != nil {
return nil, err
}
return &u, nil
}
// DeleteUser removes a user by ID.
func (s *UserService) DeleteUser(ctx context.Context, id platform.ID) error {
url, err := newURL(s.Addr, userIDPath(id))
if err != nil {
return err
}
req, err := http.NewRequest("DELETE", url.String(), nil)
if err != nil {
return err
}
req.Header.Set("Authorization", s.Token)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
var reqErr errors.Error
if err := dec.Decode(&reqErr); err != nil {
return err
}
return reqErr
}
return nil
}
func userIDPath(id platform.ID) string {
return userPath + "/" + id.String()
}

55
id_generator.go Normal file
View File

@ -0,0 +1,55 @@
package platform
import (
"encoding/hex"
"encoding/json"
)
// ID is a unique identifier.
type ID []byte
// Decode parses b as a hex-encoded byte-slice-string.
func (i *ID) Decode(b []byte) error {
dst := make([]byte, hex.DecodedLen(len(b)))
_, err := hex.Decode(dst, b)
if err != nil {
return err
}
*i = dst
return nil
}
// DecodeFromString parses s as a hex-encoded string.
func (i *ID) DecodeFromString(s string) error {
return i.Decode([]byte(s))
}
// Encode converts ID to a hex-encoded byte-slice-string.
func (i ID) Encode() []byte {
dst := make([]byte, hex.EncodedLen(len(i)))
hex.Encode(dst, i)
return dst
}
// String returns the ID as a hex encoded string
func (i ID) String() string {
return string(i.Encode())
}
// IDGenerator represents a generator for IDs.
type IDGenerator interface {
// ID creates unique byte slice ID.
ID() ID
}
// UnmarshalJSON implements JSON unmarshaller for IDs.
func (i *ID) UnmarshalJSON(b []byte) error {
b = b[1 : len(b)-1]
return i.Decode(b)
}
// MarshalJSON implements JSON marshaller for IDs.
func (i ID) MarshalJSON() ([]byte, error) {
id := i.Encode()
return json.Marshal(string(id[:]))
}

74
kit/errors/errors.go Normal file
View File

@ -0,0 +1,74 @@
package errors
import (
"fmt"
"net/http"
)
const (
// InternalError indicates an unexpected error condition.
InternalError = 1
// MalformedData indicates malformed input, such as unparsable JSON.
MalformedData = 2
// InvalidData indicates that data is well-formed, but invalid.
InvalidData = 3
// Forbidden indicates a forbidden operation.
Forbidden = 4
)
// Error indicates an error with a reference code and an HTTP status code.
type Error struct {
Reference int `json:"referenceCode"`
Code int `json:"statusCode"`
Err string `json:"err"`
}
// SetCode sets the http status code for an error.
func (e *Error) SetCode() {
switch e.Reference {
case InternalError:
e.Code = http.StatusInternalServerError
case InvalidData:
e.Code = http.StatusUnprocessableEntity
case MalformedData:
e.Code = http.StatusBadRequest
case Forbidden:
e.Code = http.StatusForbidden
default:
e.Reference = InternalError
e.Code = http.StatusInternalServerError
}
}
// Error implements the error interface.
func (e Error) Error() string {
return fmt.Sprintf("%v (error reference code: %d)", e.Err, e.Reference)
}
// Errorf constructs an Error with the given reference code and format.
func Errorf(ref int, format string, i ...interface{}) error {
return &Error{
Reference: ref,
Err: fmt.Sprintf(format, i...),
}
}
// InternalErrorf constructs an InternalError with the given format.
func InternalErrorf(format string, i ...interface{}) error {
return Errorf(InternalError, format, i...)
}
// MalformedDataf constructs a MalformedData error with the given format.
func MalformedDataf(format string, i ...interface{}) error {
return Errorf(MalformedData, format, i...)
}
// InvalidDataf constructs an InvalidData error with the given format.
func InvalidDataf(format string, i ...interface{}) error {
return Errorf(InvalidData, format, i...)
}
// Forbiddenf constructs a Forbidden error with the given format.
func Forbiddenf(format string, i ...interface{}) error {
return Errorf(Forbidden, format, i...)
}

28
kit/errors/http.go Normal file
View File

@ -0,0 +1,28 @@
package errors
import (
"context"
"fmt"
"net/http"
)
// EncodeHTTP encodes err with the appropriate status code and format,
// sets the X-Influx-Error and X-Influx-Reference headers on the response,
// and sets the response status to the corresponding status code.
func EncodeHTTP(ctx context.Context, err error, w http.ResponseWriter) {
if err == nil {
return
}
e, ok := err.(*Error)
if !ok {
e = &Error{
Reference: InternalError,
Err: err.Error(),
}
}
e.SetCode()
w.Header().Set("X-Influx-Error", e.Error())
w.Header().Set("X-Influx-Reference", fmt.Sprintf("%d", e.Reference))
w.WriteHeader(e.Code)
}

82
mock/bucket_service.go Normal file
View File

@ -0,0 +1,82 @@
package mock
import (
"context"
"github.com/influxdata/platform"
"go.uber.org/zap"
)
// BucketService is a mock implementation of a retention.BucketService, which
// also makes it a suitable mock to use wherever an platform.BucketService is required.
type BucketService struct {
// Methods for a retention.BucketService
OpenFn func() error
CloseFn func() error
WithLoggerFn func(l *zap.Logger)
// Methods for an platform.BucketService
FindBucketByIDFn func(context.Context, platform.ID) (*platform.Bucket, error)
FindBucketFn func(context.Context, platform.BucketFilter) (*platform.Bucket, error)
FindBucketsFn func(context.Context, platform.BucketFilter, ...platform.FindOptions) ([]*platform.Bucket, int, error)
CreateBucketFn func(context.Context, *platform.Bucket) error
UpdateBucketFn func(context.Context, platform.ID, platform.BucketUpdate) (*platform.Bucket, error)
DeleteBucketFn func(context.Context, platform.ID) error
}
// NewBucketService returns an mock BucketService where its methods will return
// zero values.
func NewBucketService() *BucketService {
return &BucketService{
OpenFn: func() error { return nil },
CloseFn: func() error { return nil },
WithLoggerFn: func(l *zap.Logger) {},
FindBucketByIDFn: func(context.Context, platform.ID) (*platform.Bucket, error) { return nil, nil },
FindBucketFn: func(context.Context, platform.BucketFilter) (*platform.Bucket, error) { return nil, nil },
FindBucketsFn: func(context.Context, platform.BucketFilter, ...platform.FindOptions) ([]*platform.Bucket, int, error) {
return nil, 0, nil
},
CreateBucketFn: func(context.Context, *platform.Bucket) error { return nil },
UpdateBucketFn: func(context.Context, platform.ID, platform.BucketUpdate) (*platform.Bucket, error) { return nil, nil },
DeleteBucketFn: func(context.Context, platform.ID) error { return nil },
}
}
// Open opens the BucketService.
func (s *BucketService) Open() error { return s.OpenFn() }
// Close closes the BucketService.
func (s *BucketService) Close() error { return s.CloseFn() }
// WithLogger sets the logger on the BucketService.
func (s *BucketService) WithLogger(l *zap.Logger) { s.WithLoggerFn(l) }
// FindBucketByID returns a single bucket by ID.
func (s *BucketService) FindBucketByID(ctx context.Context, id platform.ID) (*platform.Bucket, error) {
return s.FindBucketByIDFn(ctx, id)
}
// FindBucket returns the first bucket that matches filter.
func (s *BucketService) FindBucket(ctx context.Context, filter platform.BucketFilter) (*platform.Bucket, error) {
return s.FindBucketFn(ctx, filter)
}
// FindBuckets returns a list of buckets that match filter and the total count of matching buckets.
func (s *BucketService) FindBuckets(ctx context.Context, filter platform.BucketFilter, opts ...platform.FindOptions) ([]*platform.Bucket, int, error) {
return s.FindBucketsFn(ctx, filter, opts...)
}
// CreateBucket creates a new bucket and sets b.ID with the new identifier.
func (s *BucketService) CreateBucket(ctx context.Context, bucket *platform.Bucket) error {
return s.CreateBucketFn(ctx, bucket)
}
// UpdateBucket updates a single bucket with changeset.
func (s *BucketService) UpdateBucket(ctx context.Context, id platform.ID, upd platform.BucketUpdate) (*platform.Bucket, error) {
return s.UpdateBucketFn(ctx, id, upd)
}
// DeleteBucket removes a bucket by ID.
func (s *BucketService) DeleteBucket(ctx context.Context, id platform.ID) error {
return s.DeleteBucket(ctx, id)
}

43
mock/generators.go Normal file
View File

@ -0,0 +1,43 @@
package mock
import (
"github.com/influxdata/platform"
)
// IDGenerator is mock implementation of platform.IDGenerator.
type IDGenerator struct {
IDFn func() platform.ID
}
// ID generates a new platform.ID from a mock function.
func (g IDGenerator) ID() platform.ID {
return g.IDFn()
}
// NewIDGenerator is a simple way to create immutable id generator
func NewIDGenerator(s string) IDGenerator {
return IDGenerator{
IDFn: func() platform.ID {
return platform.ID(s)
},
}
}
// NewTokenGenerator is a simple way to create immutable token generator.
func NewTokenGenerator(s string, err error) TokenGenerator {
return TokenGenerator{
TokenFn: func() (string, error) {
return s, err
},
}
}
// TokenGenerator is mock implementation of platform.TokenGenerator.
type TokenGenerator struct {
TokenFn func() (string, error)
}
// Token generates a new platform.Token from a mock function.
func (g TokenGenerator) Token() (string, error) {
return g.TokenFn()
}

44
organization.go Normal file
View File

@ -0,0 +1,44 @@
package platform
import "context"
// Organization is a organization. 🎉
type Organization struct {
ID ID `json:"id"`
Name string `json:"name"`
}
// OrganizationService represents a service for managing organization data.
type OrganizationService interface {
// Returns a single organization by ID.
FindOrganizationByID(ctx context.Context, id ID) (*Organization, error)
// Returns the first organization that matches filter.
FindOrganization(ctx context.Context, filter OrganizationFilter) (*Organization, error)
// Returns a list of organizations that match filter and the total count of matching organizations.
// Additional options provide pagination & sorting.
FindOrganizations(ctx context.Context, filter OrganizationFilter, opt ...FindOptions) ([]*Organization, int, error)
// Creates a new organization and sets b.ID with the new identifier.
CreateOrganization(ctx context.Context, b *Organization) error
// Updates a single organization with changeset.
// Returns the new organization state after update.
UpdateOrganization(ctx context.Context, id ID, upd OrganizationUpdate) (*Organization, error)
// Removes a organization by ID.
DeleteOrganization(ctx context.Context, id ID) error
}
// OrganizationUpdate represents updates to a organization.
// Only fields which are set are updated.
type OrganizationUpdate struct {
Name *string
}
// OrganizationFilter represents a set of filter that restrict the returned results.
type OrganizationFilter struct {
Name *string
ID *ID
}

102
prometheus/auth_service.go Normal file
View File

@ -0,0 +1,102 @@
package prometheus
import (
"context"
"fmt"
"time"
"github.com/influxdata/platform"
"github.com/prometheus/client_golang/prometheus"
)
// AuthorizationService manages authorizations.
type AuthorizationService struct {
requestCount *prometheus.CounterVec
requestDuration *prometheus.HistogramVec
AuthorizationService platform.AuthorizationService
}
// NewAuthorizationService creates an instance of AuthorizationService.
func NewAuthorizationService() *AuthorizationService {
// TODO: what to make these values
namespace := "auth"
subsystem := "prometheus"
s := &AuthorizationService{
requestCount: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "requests_total",
Help: "Number of http requests received",
}, []string{"method", "error"}),
requestDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "request_duration_seconds",
Help: "Time taken to respond to HTTP request",
// TODO(desa): determine what spacing these buckets should have.
Buckets: prometheus.ExponentialBuckets(0.001, 1.5, 25),
}, []string{"method", "error"}),
}
prometheus.MustRegister(
s.requestCount,
s.requestDuration,
)
return s
}
// FindAuthorizationByToken returns an authorization given a token, records function call latency, and counts function calls.
func (s *AuthorizationService) FindAuthorizationByToken(ctx context.Context, t string) (a *platform.Authorization, err error) {
defer func(start time.Time) {
labels := prometheus.Labels{
"method": "FindAuthorizationsByToken",
"error": fmt.Sprint(err != nil),
}
s.requestCount.With(labels).Add(1)
s.requestDuration.With(labels).Observe(time.Since(start).Seconds())
}(time.Now())
return s.AuthorizationService.FindAuthorizationByToken(ctx, t)
}
// FindAuthorizations returns authorizations given a filter, records function call latency, and counts function calls.
func (s *AuthorizationService) FindAuthorizations(ctx context.Context, filter platform.AuthorizationFilter, opt ...platform.FindOptions) (as []*platform.Authorization, i int, err error) {
defer func(start time.Time) {
labels := prometheus.Labels{
"method": "FindAuthorizations",
"error": fmt.Sprint(err != nil),
}
s.requestCount.With(labels).Add(1)
s.requestDuration.With(labels).Observe(time.Since(start).Seconds())
}(time.Now())
return s.AuthorizationService.FindAuthorizations(ctx, filter, opt...)
}
// CreateAuthorization creates an authorization, records function call latency, and counts function calls.
func (s *AuthorizationService) CreateAuthorization(ctx context.Context, a *platform.Authorization) (err error) {
defer func(start time.Time) {
labels := prometheus.Labels{
"method": "CreateAuthorization",
"error": fmt.Sprint(err != nil),
}
s.requestCount.With(labels).Add(1)
s.requestDuration.With(labels).Observe(time.Since(start).Seconds())
}(time.Now())
return s.AuthorizationService.CreateAuthorization(ctx, a)
}
// DeleteAuthorization deletes an authorization, records function call latency, and counts function calls.
func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, t string) (err error) {
defer func(start time.Time) {
labels := prometheus.Labels{
"method": "DeleteAuthorization",
"error": fmt.Sprint(err != nil),
}
s.requestCount.With(labels).Add(1)
s.requestDuration.With(labels).Observe(time.Since(start).Seconds())
}(time.Now())
return s.AuthorizationService.DeleteAuthorization(ctx, t)
}

39
rand/token_generator.go Normal file
View File

@ -0,0 +1,39 @@
package rand
import (
"crypto/rand"
"encoding/base64"
"github.com/influxdata/platform"
)
// TokenGenerator implements platform.TokenGenerator.
type TokenGenerator struct {
size int
}
// NewTokenGenerator creates an instance of an platform.TokenGenerator.
func NewTokenGenerator(n int) platform.TokenGenerator {
return &TokenGenerator{
size: n,
}
}
// Token returns a new string token of size t.size.
func (t *TokenGenerator) Token() (string, error) {
return generateRandomString(t.size)
}
func generateRandomString(s int) (string, error) {
b, err := generateRandomBytes(s)
return base64.URLEncoding.EncodeToString(b), err
}
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return nil, err
}
return b, nil
}

32
snowflake/id_generator.go Normal file
View File

@ -0,0 +1,32 @@
package snowflake
import (
"encoding/binary"
"math/rand"
"time"
"github.com/influxdata/platform"
"github.com/influxdata/influxdb/pkg/snowflake"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
type IDGenerator struct {
Generator *snowflake.Generator
}
func NewIDGenerator() *IDGenerator {
return &IDGenerator{
// Maximum machine id is 1023
Generator: snowflake.New(rand.Intn(1023)),
}
}
func (g *IDGenerator) ID() platform.ID {
id := make(platform.ID, 8)
i := g.Generator.Next()
binary.BigEndian.PutUint64(id, i)
return id
}

View File

@ -0,0 +1,27 @@
package snowflake
import (
"bytes"
"testing"
"github.com/influxdata/platform"
)
func TestIDLength(t *testing.T) {
gen := NewIDGenerator()
id := gen.ID()
if len(id) != 8 {
t.Fail()
}
}
func TestToFromString(t *testing.T) {
gen := NewIDGenerator()
id := gen.ID()
var clone platform.ID
if err := clone.DecodeFromString(id.String()); err != nil {
t.Error(err)
} else if !bytes.Equal(id, clone) {
t.Errorf("id started as %x but got back %x", id, clone)
}
}

622
testing/user_service.go Normal file
View File

@ -0,0 +1,622 @@
package testing
import (
"bytes"
"context"
"fmt"
"sort"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/platform"
"github.com/influxdata/platform/mock"
)
var userCmpOptions = cmp.Options{
cmp.Comparer(func(x, y []byte) bool {
return bytes.Equal(x, y)
}),
cmp.Transformer("Sort", func(in []*platform.User) []*platform.User {
out := append([]*platform.User(nil), in...) // Copy input to avoid mutating it
sort.Slice(out, func(i, j int) bool {
return out[i].ID.String() > out[j].ID.String()
})
return out
}),
}
// UserFields will include the IDGenerator, and users
type UserFields struct {
IDGenerator platform.IDGenerator
Users []*platform.User
}
// CreateUser testing
func CreateUser(
init func(UserFields, *testing.T) (platform.UserService, func()),
t *testing.T,
) {
type args struct {
user *platform.User
}
type wants struct {
err error
users []*platform.User
}
tests := []struct {
name string
fields UserFields
args args
wants wants
}{
{
name: "create users with empty set",
fields: UserFields{
IDGenerator: mock.NewIDGenerator("id1"),
Users: []*platform.User{},
},
args: args{
user: &platform.User{
Name: "name1",
},
},
wants: wants{
users: []*platform.User{
{
Name: "name1",
ID: platform.ID("id1"),
},
},
},
},
{
name: "basic create user",
fields: UserFields{
IDGenerator: &mock.IDGenerator{
IDFn: func() platform.ID {
return platform.ID("2")
},
},
Users: []*platform.User{
{
ID: platform.ID("1"),
Name: "user1",
},
},
},
args: args{
user: &platform.User{
Name: "user2",
},
},
wants: wants{
users: []*platform.User{
{
ID: platform.ID("1"),
Name: "user1",
},
{
ID: platform.ID("2"),
Name: "user2",
},
},
},
},
{
name: "names should be unique",
fields: UserFields{
IDGenerator: &mock.IDGenerator{
IDFn: func() platform.ID {
return platform.ID("2")
},
},
Users: []*platform.User{
{
ID: platform.ID("1"),
Name: "user1",
},
},
},
args: args{
user: &platform.User{
Name: "user1",
},
},
wants: wants{
users: []*platform.User{
{
ID: platform.ID("1"),
Name: "user1",
},
},
err: fmt.Errorf("user with name user1 already exists"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
err := s.CreateUser(ctx, tt.args.user)
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
}
}
defer s.DeleteUser(ctx, tt.args.user.ID)
users, _, err := s.FindUsers(ctx, platform.UserFilter{})
if err != nil {
t.Fatalf("failed to retrieve users: %v", err)
}
if diff := cmp.Diff(users, tt.wants.users, userCmpOptions...); diff != "" {
t.Errorf("users are different -got/+want\ndiff %s", diff)
}
})
}
}
// FindUserByID testing
func FindUserByID(
init func(UserFields, *testing.T) (platform.UserService, func()),
t *testing.T,
) {
type args struct {
id platform.ID
}
type wants struct {
err error
user *platform.User
}
tests := []struct {
name string
fields UserFields
args args
wants wants
}{
{
name: "basic find user by id",
fields: UserFields{
Users: []*platform.User{
{
ID: platform.ID("1"),
Name: "user1",
},
{
ID: platform.ID("2"),
Name: "user2",
},
},
},
args: args{
id: platform.ID("2"),
},
wants: wants{
user: &platform.User{
ID: platform.ID("2"),
Name: "user2",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
user, err := s.FindUserByID(ctx, tt.args.id)
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected errors to be equal '%v' got '%v'", tt.wants.err, err)
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
}
}
if diff := cmp.Diff(user, tt.wants.user, userCmpOptions...); diff != "" {
t.Errorf("user is different -got/+want\ndiff %s", diff)
}
})
}
}
// FindUsers testing
func FindUsers(
init func(UserFields, *testing.T) (platform.UserService, func()),
t *testing.T,
) {
type args struct {
ID string
name string
}
type wants struct {
users []*platform.User
err error
}
tests := []struct {
name string
fields UserFields
args args
wants wants
}{
{
name: "find all users",
fields: UserFields{
Users: []*platform.User{
{
ID: platform.ID("test1"),
Name: "abc",
},
{
ID: platform.ID("test2"),
Name: "xyz",
},
},
},
args: args{},
wants: wants{
users: []*platform.User{
{
ID: platform.ID("test1"),
Name: "abc",
},
{
ID: platform.ID("test2"),
Name: "xyz",
},
},
},
},
{
name: "find user by id",
fields: UserFields{
Users: []*platform.User{
{
ID: platform.ID("test1"),
Name: "abc",
},
{
ID: platform.ID("test2"),
Name: "xyz",
},
},
},
args: args{
ID: "test2",
},
wants: wants{
users: []*platform.User{
{
ID: platform.ID("test2"),
Name: "xyz",
},
},
},
},
{
name: "find user by name",
fields: UserFields{
Users: []*platform.User{
{
ID: platform.ID("test1"),
Name: "abc",
},
{
ID: platform.ID("test2"),
Name: "xyz",
},
},
},
args: args{
name: "xyz",
},
wants: wants{
users: []*platform.User{
{
ID: platform.ID("test2"),
Name: "xyz",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
filter := platform.UserFilter{}
if tt.args.ID != "" {
id := platform.ID(tt.args.ID)
filter.ID = &id
}
if tt.args.name != "" {
filter.Name = &tt.args.name
}
users, _, err := s.FindUsers(ctx, filter)
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected errors to be equal '%v' got '%v'", tt.wants.err, err)
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
}
}
if diff := cmp.Diff(users, tt.wants.users, userCmpOptions...); diff != "" {
t.Errorf("users are different -got/+want\ndiff %s", diff)
}
})
}
}
// DeleteUser testing
func DeleteUser(
init func(UserFields, *testing.T) (platform.UserService, func()),
t *testing.T,
) {
type args struct {
ID string
}
type wants struct {
err error
users []*platform.User
}
tests := []struct {
name string
fields UserFields
args args
wants wants
}{
{
name: "delete users using exist id",
fields: UserFields{
Users: []*platform.User{
{
Name: "orgA",
ID: platform.ID("abc"),
},
{
Name: "orgB",
ID: platform.ID("xyz"),
},
},
},
args: args{
ID: "abc",
},
wants: wants{
users: []*platform.User{
{
Name: "orgB",
ID: platform.ID("xyz"),
},
},
},
},
{
name: "delete users using exist id",
fields: UserFields{
Users: []*platform.User{
{
Name: "orgA",
ID: platform.ID("abc"),
},
{
Name: "orgB",
ID: platform.ID("xyz"),
},
},
},
args: args{
ID: "123",
},
wants: wants{
users: []*platform.User{
{
Name: "orgA",
ID: platform.ID("abc"),
},
{
Name: "orgB",
ID: platform.ID("xyz"),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
err := s.DeleteUser(ctx, platform.ID(tt.args.ID))
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
}
}
filter := platform.UserFilter{}
users, _, err := s.FindUsers(ctx, filter)
if err != nil {
t.Fatalf("failed to retrieve users: %v", err)
}
if diff := cmp.Diff(users, tt.wants.users, userCmpOptions...); diff != "" {
t.Errorf("users are different -got/+want\ndiff %s", diff)
}
})
}
}
// FindUser testing
func FindUser(
init func(UserFields, *testing.T) (platform.UserService, func()),
t *testing.T,
) {
type args struct {
name string
}
type wants struct {
user *platform.User
err error
}
tests := []struct {
name string
fields UserFields
args args
wants wants
}{
{
name: "find user by name",
fields: UserFields{
Users: []*platform.User{
{
ID: platform.ID("a"),
Name: "abc",
},
{
ID: platform.ID("b"),
Name: "xyz",
},
},
},
args: args{
name: "abc",
},
wants: wants{
user: &platform.User{
ID: platform.ID("a"),
Name: "abc",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
filter := platform.UserFilter{}
if tt.args.name != "" {
filter.Name = &tt.args.name
}
user, err := s.FindUser(ctx, filter)
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
}
}
if diff := cmp.Diff(user, tt.wants.user, userCmpOptions...); diff != "" {
t.Errorf("users are different -got/+want\ndiff %s", diff)
}
})
}
}
// UpdateUser testing
func UpdateUser(
init func(UserFields, *testing.T) (platform.UserService, func()),
t *testing.T,
) {
type args struct {
name string
id platform.ID
}
type wants struct {
err error
user *platform.User
}
tests := []struct {
name string
fields UserFields
args args
wants wants
}{
{
name: "basic find all users",
fields: UserFields{
Users: []*platform.User{
{
ID: platform.ID("1"),
Name: "user1",
},
{
ID: platform.ID("2"),
Name: "user2",
},
},
},
args: args{
id: platform.ID("1"),
name: "changed",
},
wants: wants{
user: &platform.User{
ID: platform.ID("1"),
Name: "changed",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, done := init(tt.fields, t)
defer done()
ctx := context.TODO()
upd := platform.UserUpdate{
Name: &tt.args.name,
}
user, err := s.UpdateUser(ctx, tt.args.id, upd)
if (err != nil) != (tt.wants.err != nil) {
t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err)
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
}
}
if diff := cmp.Diff(user, tt.wants.user, userCmpOptions...); diff != "" {
t.Errorf("user is different -got/+want\ndiff %s", diff)
}
})
}
}

7
token_generator.go Normal file
View File

@ -0,0 +1,7 @@
package platform
// TokenGenerator represents a generator for API tokens.
type TokenGenerator interface {
// Token generates a new API token.
Token() (string, error)
}

42
usage.go Normal file
View File

@ -0,0 +1,42 @@
package platform
import (
"context"
"time"
)
// UsageMetric used to track classes of usage.
type UsageMetric string
const (
// UsageWriteRequestCount is the name of the metrics for tracking write request count.
UsageWriteRequestCount UsageMetric = "usage_write_request_count"
// UsageWriteRequestBytes is the name of the metrics for tracking the number of bytes.
UsageWriteRequestBytes UsageMetric = "usage_write_request_bytes"
)
// Usage is a metric associated with the utilization of a particular resource.
type Usage struct {
OrganizationID *ID `json:"organizationID,omitempty"`
BucketID *ID `json:"bucketID,omitempty"`
Type UsageMetric `json:"type"`
Value float64 `json:"value"`
}
// UsageService is a service for accessing usage statistics.
type UsageService interface {
GetUsage(ctx context.Context, filter UsageFilter) (map[UsageMetric]*Usage, error)
}
// UsageFilter is used to filter usage.
type UsageFilter struct {
OrgID *ID
BucketID *ID
Range *Timespan
}
// Timespan represents a range of time.
type Timespan struct {
Start time.Time `json:"start"`
Stop time.Time `json:"stop"`
}

44
user.go Normal file
View File

@ -0,0 +1,44 @@
package platform
import "context"
// User is a user. 🎉
type User struct {
ID ID `json:"id,omitempty"`
Name string `json:"name"`
}
// UserService represents a service for managing user data.
type UserService interface {
// Returns a single user by ID.
FindUserByID(ctx context.Context, id ID) (*User, error)
// Returns the first user that matches filter.
FindUser(ctx context.Context, filter UserFilter) (*User, error)
// Returns a list of users that match filter and the total count of matching users.
// Additional options provide pagination & sorting.
FindUsers(ctx context.Context, filter UserFilter, opt ...FindOptions) ([]*User, int, error)
// Creates a new user and sets u.ID with the new identifier.
CreateUser(ctx context.Context, u *User) error
// Updates a single user with changeset.
// Returns the new user state after update.
UpdateUser(ctx context.Context, id ID, upd UserUpdate) (*User, error)
// Removes a user by ID.
DeleteUser(ctx context.Context, id ID) error
}
// UserUpdate represents updates to a user.
// Only fields which are set are updated.
type UserUpdate struct {
Name *string `json:"name"`
}
// UserFilter represents a set of filter that restrict the returned results.
type UserFilter struct {
ID *ID
Name *string
}

59
zap/auth_service.go Normal file
View File

@ -0,0 +1,59 @@
package logger
import (
"context"
"go.uber.org/zap"
"github.com/influxdata/platform"
)
// AuthorizationService manages authorizations.
type AuthorizationService struct {
Logger *zap.Logger
AuthorizationService platform.AuthorizationService
}
// FindAuthorizationByToken returns an authorization given a token, and logs any errors.
func (s *AuthorizationService) FindAuthorizationByToken(ctx context.Context, t string) (a *platform.Authorization, err error) {
defer func() {
if err != nil {
s.Logger.Info("error finding authorization by token", zap.Error(err))
}
}()
return s.AuthorizationService.FindAuthorizationByToken(ctx, t)
}
// FindAuthorizations returns authorizations given a filter, and logs any errors.
func (s *AuthorizationService) FindAuthorizations(ctx context.Context, filter platform.AuthorizationFilter, opt ...platform.FindOptions) (as []*platform.Authorization, i int, err error) {
defer func() {
if err != nil {
s.Logger.Info("error finding authorizations", zap.Error(err))
}
}()
return s.AuthorizationService.FindAuthorizations(ctx, filter, opt...)
}
// CreateAuthorization creates an authorization, and logs any errors.
func (s *AuthorizationService) CreateAuthorization(ctx context.Context, a *platform.Authorization) (err error) {
defer func() {
if err != nil {
s.Logger.Info("error creating authorization", zap.Error(err))
}
}()
return s.AuthorizationService.CreateAuthorization(ctx, a)
}
// DeleteAuthorization deletes an authorization, and logs any errors.
func (s *AuthorizationService) DeleteAuthorization(ctx context.Context, t string) (err error) {
defer func() {
if err != nil {
s.Logger.Info("error deleting authorization", zap.Error(err))
}
}()
return s.AuthorizationService.DeleteAuthorization(ctx, t)
}