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 againpull/10616/head
parent
6982acfa37
commit
eabba6986d
|
@ -0,0 +1,6 @@
|
|||
vendor
|
||||
.netrc
|
||||
|
||||
# Project binaries.
|
||||
/idp
|
||||
/transpilerd
|
|
@ -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
|
|
@ -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
|
|
@ -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)).
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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}
|
||||
)
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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[:]))
|
||||
}
|
|
@ -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...)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue