feat: dbrp service

Signed-off-by: Lorenzo Affetti <lorenzo.affetti@gmail.com>
Co-Authored-By: Gianluca Arbezzano <gianarb92@gmail.com>
Co-Authored-By: George MacRorie <gmacrorie@influxdata.com>
Co-Authored-By: Alirie Gray <alirie.gray@gmail.com>
pull/17800/head
Gianluca Arbezzano 2020-04-20 18:55:23 +02:00 committed by Lorenzo Affetti
parent f646653b1b
commit 1cf64fd721
No known key found for this signature in database
GPG Key ID: 76FFBBDF6F0ADC4C
19 changed files with 4563 additions and 13 deletions

View File

@ -6,6 +6,24 @@ import (
"github.com/influxdata/influxdb/v2"
)
// AuthorizeFindDBRPs takes the given items and returns only the ones that the user is authorized to read.
func AuthorizeFindDBRPs(ctx context.Context, rs []*influxdb.DBRPMappingV2) ([]*influxdb.DBRPMappingV2, int, error) {
// This filters without allocating
// https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
rrs := rs[:0]
for _, r := range rs {
_, _, err := AuthorizeRead(ctx, influxdb.DBRPResourceType, r.ID, r.OrganizationID)
if err != nil && influxdb.ErrorCode(err) != influxdb.EUnauthorized {
return nil, 0, err
}
if influxdb.ErrorCode(err) == influxdb.EUnauthorized {
continue
}
rrs = append(rrs, r)
}
return rrs, len(rrs), nil
}
// AuthorizeFindAuthorizations takes the given items and returns only the ones that the user is authorized to read.
func AuthorizeFindAuthorizations(ctx context.Context, rs []*influxdb.Authorization) ([]*influxdb.Authorization, int, error) {
// This filters without allocating

View File

@ -129,6 +129,8 @@ const (
NotificationEndpointResourceType = ResourceType("notificationEndpoints") // 15
// ChecksResourceType gives permission to one or more Checks.
ChecksResourceType = ResourceType("checks") // 16
// DBRPType gives permission to one or more DBRPs.
DBRPResourceType = ResourceType("dbrp") // 17
)
// AllResourceTypes is the list of all known resource types.
@ -150,6 +152,7 @@ var AllResourceTypes = []ResourceType{
NotificationRuleResourceType, // 14
NotificationEndpointResourceType, // 15
ChecksResourceType, // 16
DBRPResourceType, // 17
// NOTE: when modifying this list, please update the swagger for components.schemas.Permission resource enum.
}
@ -167,6 +170,7 @@ var OrgResourceTypes = []ResourceType{
NotificationRuleResourceType, // 14
NotificationEndpointResourceType, // 15
ChecksResourceType, // 16
DBRPResourceType, // 17
}
// Valid checks if the resource type is a member of the ResourceType enum.
@ -194,6 +198,7 @@ func (t ResourceType) Valid() (err error) {
case NotificationRuleResourceType: // 14
case NotificationEndpointResourceType: // 15
case ChecksResourceType: // 16
case DBRPResourceType: // 17
default:
err = ErrInvalidResourceType
}

View File

@ -75,6 +75,9 @@ var authCreateFlags struct {
writeNotificationEndpointPermission bool
readNotificationEndpointPermission bool
writeDBRPPermission bool
readDBRPPermission bool
}
func authCreateCmd() *cobra.Command {
@ -118,6 +121,9 @@ func authCreateCmd() *cobra.Command {
cmd.Flags().BoolVarP(&authCreateFlags.writeCheckPermission, "write-checks", "", false, "Grants the permission to create checks")
cmd.Flags().BoolVarP(&authCreateFlags.readCheckPermission, "read-checks", "", false, "Grants the permission to read checks")
cmd.Flags().BoolVarP(&authCreateFlags.writeDBRPPermission, "write-dbrps", "", false, "Grants the permission to create database retention policy mappings")
cmd.Flags().BoolVarP(&authCreateFlags.readDBRPPermission, "read-dbrps", "", false, "Grants the permission to read database retention policy mappings")
return cmd
}
@ -216,6 +222,11 @@ func authorizationCreateF(cmd *cobra.Command, args []string) error {
writePerm: authCreateFlags.writeUserPermission,
ResourceType: platform.UsersResourceType,
},
{
readPerm: authCreateFlags.readDBRPPermission,
writePerm: authCreateFlags.writeDBRPPermission,
ResourceType: platform.DBRPResourceType,
},
}
for _, provided := range providedPerm {

View File

@ -22,6 +22,7 @@ import (
"github.com/influxdata/influxdb/v2/bolt"
"github.com/influxdata/influxdb/v2/chronograf/server"
"github.com/influxdata/influxdb/v2/cmd/influxd/inspect"
"github.com/influxdata/influxdb/v2/dbrp"
"github.com/influxdata/influxdb/v2/endpoints"
"github.com/influxdata/influxdb/v2/gather"
"github.com/influxdata/influxdb/v2/http"
@ -762,6 +763,13 @@ func (m *Launcher) run(ctx context.Context) (err error) {
}
}
dbrpSvc, err := dbrp.NewService(ctx, authorizer.NewBucketService(bucketSvc, userResourceSvc), m.kvStore)
if err != nil {
return err
}
dbrpSvc = dbrp.NewAuthorizedService(dbrpSvc)
var checkSvc platform.CheckService
{
coordinator := coordinator.NewCoordinator(m.log, m.scheduler, m.executor)
@ -881,6 +889,7 @@ func (m *Launcher) run(ctx context.Context) (err error) {
BucketService: storage.NewBucketService(bucketSvc, m.engine),
SessionService: sessionSvc,
UserService: userSvc,
DBRPService: dbrpSvc,
OrganizationService: orgSvc,
UserResourceMappingService: userResourceSvc,
LabelService: labelSvc,

60
dbrp/error.go Normal file
View File

@ -0,0 +1,60 @@
package dbrp
import (
"github.com/influxdata/influxdb/v2"
)
var (
// ErrInvalidDBRPID is used when the ID of the DBRP cannot be encoded.
ErrInvalidDBRPID = &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "DBRP ID is invalid",
}
// ErrDBRPNotFound is used when the specified DBRP cannot be found.
ErrDBRPNotFound = &influxdb.Error{
Code: influxdb.ENotFound,
Msg: "unable to find DBRP",
}
// ErrNotUniqueID is used when the ID of the DBRP is not unique.
ErrNotUniqueID = &influxdb.Error{
Code: influxdb.EConflict,
Msg: "ID already exists",
}
// ErrFailureGeneratingID occurs ony when the random number generator
// cannot generate an ID in MaxIDGenerationN times.
ErrFailureGeneratingID = &influxdb.Error{
Code: influxdb.EInternal,
Msg: "unable to generate valid id",
}
)
// ErrInvalidDBRP is used when a service was provided an invalid DBRP.
func ErrInvalidDBRP(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "DBRP provided is invalid",
Err: err,
}
}
// ErrInternalService is used when the error comes from an internal system.
func ErrInternalService(err error) *influxdb.Error {
return &influxdb.Error{
Code: influxdb.EInternal,
Err: err,
}
}
// ErrDBRPAlreadyExists is used when there is a conflict in creating a new DBRP.
func ErrDBRPAlreadyExists(msg string) *influxdb.Error {
if msg == "" {
msg = "DBRP already exists"
}
return &influxdb.Error{
Code: influxdb.EConflict,
Msg: msg,
}
}

130
dbrp/http_client_dbrp.go Normal file
View File

@ -0,0 +1,130 @@
package dbrp
import (
"context"
"fmt"
"path"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kit/tracing"
"github.com/influxdata/influxdb/v2/pkg/httpc"
)
var _ influxdb.DBRPMappingServiceV2 = (*Client)(nil)
// Client connects to Influx via HTTP using tokens to manage DBRPs.
type Client struct {
Client *httpc.Client
Prefix string
}
func NewClient(client *httpc.Client) *Client {
return &Client{
Client: client,
Prefix: PrefixDBRP,
}
}
func (c *Client) dbrpURL(id influxdb.ID) string {
return path.Join(c.Prefix, id.String())
}
func (c *Client) FindByID(ctx context.Context, orgID, id influxdb.ID) (*influxdb.DBRPMappingV2, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var resp getDBRPResponse
if err := c.Client.
Get(c.dbrpURL(id)).
QueryParams([2]string{"orgID", orgID.String()}).
DecodeJSON(&resp).
Do(ctx); err != nil {
return nil, err
}
return resp.Content, nil
}
func (c *Client) FindMany(ctx context.Context, filter influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
params := influxdb.FindOptionParams(opts...)
if filter.OrgID != nil {
params = append(params, [2]string{"orgID", filter.OrgID.String()})
} else {
return nil, 0, fmt.Errorf("please filter by orgID")
}
if filter.ID != nil {
params = append(params, [2]string{"id", filter.ID.String()})
}
if filter.BucketID != nil {
params = append(params, [2]string{"bucketID", filter.BucketID.String()})
}
if filter.Database != nil {
params = append(params, [2]string{"db", *filter.Database})
}
if filter.RetentionPolicy != nil {
params = append(params, [2]string{"rp", *filter.RetentionPolicy})
}
var resp getDBRPsResponse
if err := c.Client.
Get(c.Prefix).
QueryParams(params...).
DecodeJSON(&resp).
Do(ctx); err != nil {
return nil, 0, err
}
return resp.Content, len(resp.Content), nil
}
func (c *Client) Create(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
var newDBRP influxdb.DBRPMappingV2
if err := c.Client.
PostJSON(createDBRPRequest{
Database: dbrp.Database,
RetentionPolicy: dbrp.RetentionPolicy,
Default: dbrp.Default,
OrganizationID: dbrp.OrganizationID,
BucketID: dbrp.BucketID,
}, c.Prefix).
DecodeJSON(&newDBRP).
Do(ctx); err != nil {
return err
}
dbrp.ID = newDBRP.ID
return nil
}
func (c *Client) Update(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
if err := dbrp.Validate(); err != nil {
return err
}
var newDBRP influxdb.DBRPMappingV2
if err := c.Client.
PatchJSON(dbrp, c.dbrpURL(dbrp.ID)).
QueryParams([2]string{"orgID", dbrp.OrganizationID.String()}).
DecodeJSON(&newDBRP).
Do(ctx); err != nil {
return err
}
*dbrp = newDBRP
return nil
}
func (c *Client) Delete(ctx context.Context, orgID, id influxdb.ID) error {
span, _ := tracing.StartSpanFromContext(ctx)
defer span.Finish()
return c.Client.
Delete(c.dbrpURL(id)).
QueryParams([2]string{"orgID", orgID.String()}).
Do(ctx)
}

View File

@ -0,0 +1,102 @@
package dbrp_test
import (
"context"
"net/http/httptest"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/dbrp"
"github.com/influxdata/influxdb/v2/http"
"github.com/influxdata/influxdb/v2/mock"
"github.com/influxdata/influxdb/v2/pkg/httpc"
"go.uber.org/zap/zaptest"
)
func setup(t *testing.T) (*dbrp.Client, func()) {
t.Helper()
svc := &mock.DBRPMappingServiceV2{
CreateFn: func(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
dbrp.ID = 1
return nil
},
FindByIDFn: func(ctx context.Context, orgID, id influxdb.ID) (*influxdb.DBRPMappingV2, error) {
return &influxdb.DBRPMappingV2{
ID: id,
Database: "db",
RetentionPolicy: "rp",
Default: false,
OrganizationID: id,
BucketID: 1,
}, nil
},
FindManyFn: func(ctx context.Context, dbrp influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
return []*influxdb.DBRPMappingV2{}, 0, nil
},
}
server := httptest.NewServer(dbrp.NewHTTPHandler(zaptest.NewLogger(t), svc))
client, err := httpc.New(httpc.WithAddr(server.URL), httpc.WithStatusFn(http.CheckError))
if err != nil {
t.Fatal(err)
}
dbrpClient := dbrp.NewClient(client)
dbrpClient.Prefix = ""
return dbrpClient, func() {
server.Close()
}
}
func TestClient(t *testing.T) {
t.Run("can create", func(t *testing.T) {
client, shutdown := setup(t)
defer shutdown()
if err := client.Create(context.Background(), &influxdb.DBRPMappingV2{
Database: "db",
RetentionPolicy: "rp",
Default: false,
OrganizationID: 1,
BucketID: 1,
}); err != nil {
t.Error(err)
}
})
t.Run("can read", func(t *testing.T) {
client, shutdown := setup(t)
defer shutdown()
if _, err := client.FindByID(context.Background(), 1, 1); err != nil {
t.Error(err)
}
oid := influxdb.ID(1)
if _, _, err := client.FindMany(context.Background(), influxdb.DBRPMappingFilterV2{OrgID: &oid}); err != nil {
t.Error(err)
}
})
t.Run("can update", func(t *testing.T) {
client, shutdown := setup(t)
defer shutdown()
if err := client.Update(context.Background(), &influxdb.DBRPMappingV2{
ID: 1,
Database: "db",
RetentionPolicy: "rp",
Default: false,
OrganizationID: 1,
BucketID: 1,
}); err != nil {
t.Error(err)
}
})
t.Run("can delete", func(t *testing.T) {
client, shutdown := setup(t)
defer shutdown()
if err := client.Delete(context.Background(), 1, 1); err != nil {
t.Error(err)
}
})
}

309
dbrp/http_server_dbrp.go Normal file
View File

@ -0,0 +1,309 @@
package dbrp
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/influxdata/influxdb/v2"
kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
"go.uber.org/zap"
)
const (
PrefixDBRP = "/api/v2/dbrps"
)
type Handler struct {
chi.Router
api *kithttp.API
log *zap.Logger
dbrpSvc influxdb.DBRPMappingServiceV2
}
// NewHTTPHandler constructs a new http server.
func NewHTTPHandler(log *zap.Logger, dbrpSvc influxdb.DBRPMappingServiceV2) *Handler {
h := &Handler{
api: kithttp.NewAPI(kithttp.WithLog(log)),
log: log,
dbrpSvc: dbrpSvc,
}
r := chi.NewRouter()
r.Use(
middleware.Recoverer,
middleware.RequestID,
middleware.RealIP,
)
r.Route("/", func(r chi.Router) {
r.Post("/", h.handlePostDBRP)
r.Get("/", h.handleGetDBRPs)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.handleGetDBRP)
r.Patch("/", h.handlePatchDBRP)
r.Delete("/", h.handleDeleteDBRP)
})
})
h.Router = r
return h
}
type createDBRPRequest struct {
Database string `json:"database"`
RetentionPolicy string `json:"retention_policy"`
Default bool `json:"default"`
OrganizationID influxdb.ID `json:"organization_id"`
BucketID influxdb.ID `json:"bucket_id"`
}
func (h *Handler) handlePostDBRP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var req createDBRPRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.api.Err(w, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid json structure",
Err: err,
})
return
}
dbrp := &influxdb.DBRPMappingV2{
Database: req.Database,
RetentionPolicy: req.RetentionPolicy,
Default: req.Default,
OrganizationID: req.OrganizationID,
BucketID: req.BucketID,
}
if err := h.dbrpSvc.Create(ctx, dbrp); err != nil {
h.api.Err(w, err)
return
}
h.api.Respond(w, http.StatusCreated, dbrp)
}
type getDBRPsResponse struct {
Content []*influxdb.DBRPMappingV2 `json:"content"`
}
func (h *Handler) handleGetDBRPs(w http.ResponseWriter, r *http.Request) {
filter, err := getFilterFromHTTPRequest(r)
if err != nil {
h.api.Err(w, err)
return
}
dbrps, _, err := h.dbrpSvc.FindMany(r.Context(), filter)
if err != nil {
h.api.Err(w, err)
return
}
h.api.Respond(w, http.StatusOK, getDBRPsResponse{
Content: dbrps,
})
}
type getDBRPResponse struct {
Content *influxdb.DBRPMappingV2 `json:"content"`
}
func (h *Handler) handleGetDBRP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := chi.URLParam(r, "id")
if id == "" {
h.api.Err(w, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
})
return
}
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
h.api.Err(w, err)
return
}
orgID, err := mustGetOrgIDFromHTTPRequest(r)
if err != nil {
h.api.Err(w, err)
return
}
dbrp, err := h.dbrpSvc.FindByID(ctx, *orgID, i)
if err != nil {
h.api.Err(w, err)
return
}
h.api.Respond(w, http.StatusOK, getDBRPResponse{
Content: dbrp,
})
}
func (h *Handler) handlePatchDBRP(w http.ResponseWriter, r *http.Request) {
bodyRequest := struct {
Default *bool `json:"default"`
RetentionPolicy *string `json:"retention_policy"`
}{}
ctx := r.Context()
id := chi.URLParam(r, "id")
if id == "" {
h.api.Err(w, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
})
return
}
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
h.api.Err(w, err)
return
}
orgID, err := mustGetOrgIDFromHTTPRequest(r)
if err != nil {
h.api.Err(w, err)
return
}
dbrp, err := h.dbrpSvc.FindByID(ctx, *orgID, i)
if err != nil {
h.api.Err(w, err)
return
}
if err := json.NewDecoder(r.Body).Decode(&bodyRequest); err != nil {
h.api.Err(w, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid json structure",
Err: err,
})
return
}
if bodyRequest.Default != nil && dbrp.Default != *bodyRequest.Default {
dbrp.Default = *bodyRequest.Default
}
if bodyRequest.RetentionPolicy != nil && *bodyRequest.RetentionPolicy != dbrp.RetentionPolicy {
dbrp.RetentionPolicy = *bodyRequest.RetentionPolicy
}
if err := h.dbrpSvc.Update(ctx, dbrp); err != nil {
h.api.Err(w, err)
return
}
h.api.Respond(w, http.StatusOK, struct {
Content *influxdb.DBRPMappingV2 `json:"content"`
}{
Content: dbrp,
})
}
func (h *Handler) handleDeleteDBRP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := chi.URLParam(r, "id")
if id == "" {
h.api.Err(w, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "url missing id",
})
return
}
var i influxdb.ID
if err := i.DecodeFromString(id); err != nil {
h.api.Err(w, err)
return
}
orgID, err := mustGetOrgIDFromHTTPRequest(r)
if err != nil {
h.api.Err(w, err)
return
}
if err := h.dbrpSvc.Delete(ctx, *orgID, i); err != nil {
h.api.Err(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func getFilterFromHTTPRequest(r *http.Request) (f influxdb.DBRPMappingFilterV2, err error) {
// Always provide OrgID.
f.OrgID, err = mustGetOrgIDFromHTTPRequest(r)
if err != nil {
return f, err
}
f.ID, err = getDBRPIDFromHTTPRequest(r)
if err != nil {
return f, err
}
f.BucketID, err = getBucketIDFromHTTPRequest(r)
if err != nil {
return f, err
}
rawDB := r.URL.Query().Get("db")
if rawDB != "" {
f.Database = &rawDB
}
rawRP := r.URL.Query().Get("rp")
if rawRP != "" {
f.RetentionPolicy = &rawRP
}
rawDefault := r.URL.Query().Get("default")
if rawDefault != "" {
d, err := strconv.ParseBool(rawDefault)
if err != nil {
return f, &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid default parameter",
}
}
f.Default = &d
}
return f, nil
}
func getIDFromHTTPRequest(r *http.Request, key string) (*influxdb.ID, error) {
var id influxdb.ID
raw := r.URL.Query().Get(key)
if raw != "" {
if err := id.DecodeFromString(raw); err != nil {
return nil, influxdb.ErrInvalidID
}
} else {
return nil, nil
}
return &id, nil
}
func mustGetOrgIDFromHTTPRequest(r *http.Request) (*influxdb.ID, error) {
orgID, err := getIDFromHTTPRequest(r, "orgID")
if err != nil {
return nil, err
}
if orgID == nil {
return nil, influxdb.ErrOrgNotFound
}
return orgID, nil
}
func getDBRPIDFromHTTPRequest(r *http.Request) (*influxdb.ID, error) {
return getIDFromHTTPRequest(r, "id")
}
func getBucketIDFromHTTPRequest(r *http.Request) (*influxdb.ID, error) {
return getIDFromHTTPRequest(r, "bucketID")
}

View File

@ -0,0 +1,453 @@
package dbrp_test
import (
"context"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/dbrp"
"github.com/influxdata/influxdb/v2/inmem"
"github.com/influxdata/influxdb/v2/mock"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func initHttpService(t *testing.T) (influxdb.DBRPMappingServiceV2, *httptest.Server, func()) {
t.Helper()
ctx := context.Background()
bucketSvc := mock.NewBucketService()
s := inmem.NewKVStore()
svc, err := dbrp.NewService(ctx, bucketSvc, s)
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(dbrp.NewHTTPHandler(zaptest.NewLogger(t), svc))
return svc, server, func() {
server.Close()
}
}
func Test_handlePostDBRP(t *testing.T) {
table := []struct {
Name string
ExpectedErr *influxdb.Error
ExpectedDBRP *influxdb.DBRPMappingV2
Input io.Reader
}{
{
Name: "Create valid dbrp",
Input: strings.NewReader(`{
"bucket_id": "5555f7ed2a035555",
"organization_id": "059af7ed2a034000",
"database": "mydb",
"retention_policy": "autogen",
"default": false
}`),
ExpectedDBRP: &influxdb.DBRPMappingV2{
OrganizationID: influxdbtesting.MustIDBase16("059af7ed2a034000"),
},
},
{
Name: "Create with invalid orgID",
Input: strings.NewReader(`{
"bucket_id": "5555f7ed2a035555",
"organization_id": "invalid",
"database": "mydb",
"retention_policy": "autogen",
"default": false
}`),
ExpectedErr: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid json structure",
Err: influxdb.ErrInvalidID.Err,
},
},
}
for _, tt := range table {
t.Run(tt.Name, func(t *testing.T) {
if tt.ExpectedErr != nil && tt.ExpectedDBRP != nil {
t.Error("one of those has to be set")
}
_, server, shutdown := initHttpService(t)
defer shutdown()
client := server.Client()
resp, err := client.Post(server.URL+"/", "application/json", tt.Input)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if tt.ExpectedErr != nil {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(b), tt.ExpectedErr.Error()) {
t.Fatal(string(b))
}
return
}
dbrp := &influxdb.DBRPMappingV2{}
if err := json.NewDecoder(resp.Body).Decode(&dbrp); err != nil {
t.Fatal(err)
}
if !dbrp.ID.Valid() {
t.Fatalf("expected invalid id, got an invalid one %s", dbrp.ID.String())
}
if dbrp.OrganizationID != tt.ExpectedDBRP.OrganizationID {
t.Fatalf("expected orgid %s got %s", tt.ExpectedDBRP.OrganizationID, dbrp.OrganizationID)
}
})
}
}
func Test_handleGetDBRPs(t *testing.T) {
table := []struct {
Name string
QueryParams string
ExpectedErr *influxdb.Error
ExpectedDBRPs []influxdb.DBRPMappingV2
}{
{
Name: "ok",
QueryParams: "orgID=059af7ed2a034000",
ExpectedDBRPs: []influxdb.DBRPMappingV2{
{
ID: influxdbtesting.MustIDBase16("1111111111111111"),
BucketID: influxdbtesting.MustIDBase16("5555f7ed2a035555"),
OrganizationID: influxdbtesting.MustIDBase16("059af7ed2a034000"),
Database: "mydb",
RetentionPolicy: "autogen",
Default: true,
},
},
},
{
Name: "invalid org",
QueryParams: "orgID=invalid",
ExpectedErr: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid ID",
},
},
{
Name: "invalid bucket",
QueryParams: "orgID=059af7ed2a034000&bucketID=invalid",
ExpectedErr: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid ID",
},
},
{
Name: "invalid default",
QueryParams: "orgID=059af7ed2a034000&default=notabool",
ExpectedErr: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid default parameter",
},
},
{
Name: "no org",
QueryParams: "default=true&retention_police=lol",
ExpectedErr: influxdb.ErrOrgNotFound,
},
{
Name: "no match",
QueryParams: "orgID=059af7ed2a034000&default=false",
ExpectedDBRPs: []influxdb.DBRPMappingV2{},
},
{
Name: "all match",
QueryParams: "orgID=059af7ed2a034000&default=true&rp=autogen&db=mydb&bucketID=5555f7ed2a035555&id=1111111111111111",
ExpectedDBRPs: []influxdb.DBRPMappingV2{
{
ID: influxdbtesting.MustIDBase16("1111111111111111"),
BucketID: influxdbtesting.MustIDBase16("5555f7ed2a035555"),
OrganizationID: influxdbtesting.MustIDBase16("059af7ed2a034000"),
Database: "mydb",
RetentionPolicy: "autogen",
Default: true,
},
},
},
}
ctx := context.Background()
for _, tt := range table {
t.Run(tt.Name, func(t *testing.T) {
if tt.ExpectedErr != nil && len(tt.ExpectedDBRPs) != 0 {
t.Error("one of those has to be set")
}
svc, server, shutdown := initHttpService(t)
defer shutdown()
if svc, ok := svc.(*dbrp.Service); ok {
svc.IDGen = mock.NewIDGenerator("1111111111111111", t)
}
db := &influxdb.DBRPMappingV2{
BucketID: influxdbtesting.MustIDBase16("5555f7ed2a035555"),
OrganizationID: influxdbtesting.MustIDBase16("059af7ed2a034000"),
Database: "mydb",
RetentionPolicy: "autogen",
Default: true,
}
if err := svc.Create(ctx, db); err != nil {
t.Fatal(err)
}
client := server.Client()
resp, err := client.Get(server.URL + "?" + tt.QueryParams)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if tt.ExpectedErr != nil {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(b), tt.ExpectedErr.Error()) {
t.Fatal(string(b))
}
return
}
dbrps := struct {
Content []influxdb.DBRPMappingV2 `json:"content"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&dbrps); err != nil {
t.Fatal(err)
}
if len(dbrps.Content) != len(tt.ExpectedDBRPs) {
t.Fatalf("expected %d dbrps got %d", len(tt.ExpectedDBRPs), len(dbrps.Content))
}
if !cmp.Equal(tt.ExpectedDBRPs, dbrps.Content) {
t.Fatalf(cmp.Diff(tt.ExpectedDBRPs, dbrps.Content))
}
})
}
}
func Test_handlePatchDBRP(t *testing.T) {
table := []struct {
Name string
ExpectedErr *influxdb.Error
ExpectedDBRP *influxdb.DBRPMappingV2
URLSuffix string
Input io.Reader
}{
{
Name: "happy path update",
URLSuffix: "/1111111111111111?orgID=059af7ed2a034000",
Input: strings.NewReader(`{
"retention_policy": "updaterp",
"database": "wont_change"
}`),
ExpectedDBRP: &influxdb.DBRPMappingV2{
ID: influxdbtesting.MustIDBase16("1111111111111111"),
BucketID: influxdbtesting.MustIDBase16("5555f7ed2a035555"),
OrganizationID: influxdbtesting.MustIDBase16("059af7ed2a034000"),
Database: "mydb",
RetentionPolicy: "updaterp",
Default: true,
},
},
{
Name: "invalid org",
URLSuffix: "/1111111111111111?orgID=invalid",
Input: strings.NewReader(`{
"database": "updatedb"
}`),
ExpectedErr: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid ID",
},
},
{
Name: "no org",
URLSuffix: "/1111111111111111",
Input: strings.NewReader(`{
"database": "updatedb"
}`),
ExpectedErr: influxdb.ErrOrgNotFound,
},
{
Name: "not found",
URLSuffix: "/1111111111111111?orgID=059af7ed2a034001",
ExpectedErr: dbrp.ErrDBRPNotFound,
},
}
ctx := context.Background()
for _, tt := range table {
t.Run(tt.Name, func(t *testing.T) {
if tt.ExpectedErr != nil && tt.ExpectedDBRP != nil {
t.Error("one of those has to be set")
}
svc, server, shutdown := initHttpService(t)
defer shutdown()
client := server.Client()
if svc, ok := svc.(*dbrp.Service); ok {
svc.IDGen = mock.NewIDGenerator("1111111111111111", t)
}
dbrp := &influxdb.DBRPMappingV2{
BucketID: influxdbtesting.MustIDBase16("5555f7ed2a035555"),
OrganizationID: influxdbtesting.MustIDBase16("059af7ed2a034000"),
Database: "mydb",
RetentionPolicy: "autogen",
Default: true,
}
if err := svc.Create(ctx, dbrp); err != nil {
t.Fatal(err)
}
req, _ := http.NewRequest(http.MethodPatch, server.URL+tt.URLSuffix, tt.Input)
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if tt.ExpectedErr != nil {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(b), tt.ExpectedErr.Error()) {
t.Fatal(string(b))
}
return
}
dbrpResponse := struct {
Content *influxdb.DBRPMappingV2 `json:"content"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&dbrpResponse); err != nil {
t.Fatal(err)
}
if !cmp.Equal(tt.ExpectedDBRP, dbrpResponse.Content) {
t.Fatalf(cmp.Diff(tt.ExpectedDBRP, dbrpResponse.Content))
}
})
}
}
func Test_handleDeleteDBRP(t *testing.T) {
table := []struct {
Name string
URLSuffix string
ExpectedErr *influxdb.Error
ExpectStillExists bool
}{
{
Name: "delete",
URLSuffix: "/1111111111111111?orgID=059af7ed2a034000",
},
{
Name: "invalid org",
URLSuffix: "/1111111111111111?orgID=invalid",
ExpectedErr: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "invalid ID",
},
},
{
Name: "no org",
URLSuffix: "/1111111111111111",
ExpectedErr: influxdb.ErrOrgNotFound,
},
{
// Not found is not an error for Delete.
Name: "not found",
URLSuffix: "/1111111111111111?orgID=059af7ed2a034001",
ExpectStillExists: true,
},
}
ctx := context.Background()
for _, tt := range table {
t.Run(tt.Name, func(t *testing.T) {
svc, server, shutdown := initHttpService(t)
defer shutdown()
client := server.Client()
d := &influxdb.DBRPMappingV2{
ID: influxdbtesting.MustIDBase16("1111111111111111"),
BucketID: influxdbtesting.MustIDBase16("5555f7ed2a035555"),
OrganizationID: influxdbtesting.MustIDBase16("059af7ed2a034000"),
Database: "mydb",
RetentionPolicy: "autogen",
Default: true,
}
if err := svc.Create(ctx, d); err != nil {
t.Fatal(err)
}
req, _ := http.NewRequest(http.MethodDelete, server.URL+tt.URLSuffix, nil)
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if tt.ExpectedErr != nil {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(b), tt.ExpectedErr.Error()) {
t.Fatal(string(b))
}
return
}
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("expected status code %d, got %d", http.StatusNoContent, resp.StatusCode)
}
gotDBRP, err := svc.FindByID(ctx, influxdbtesting.MustIDBase16("059af7ed2a034000"), influxdbtesting.MustIDBase16("1111111111111111"))
if tt.ExpectStillExists {
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(d, gotDBRP); diff != "" {
t.Fatal(diff)
}
} else {
if err == nil {
t.Fatal("expected error got none")
}
if !errors.Is(err, dbrp.ErrDBRPNotFound) {
t.Fatalf("expected err dbrp not found, got %s", err)
}
}
})
}
}

54
dbrp/middleware_auth.go Normal file
View File

@ -0,0 +1,54 @@
package dbrp
import (
"context"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorizer"
)
var _ influxdb.DBRPMappingServiceV2 = (*AuthorizedService)(nil)
type AuthorizedService struct {
influxdb.DBRPMappingServiceV2
}
func NewAuthorizedService(s influxdb.DBRPMappingServiceV2) *AuthorizedService {
return &AuthorizedService{DBRPMappingServiceV2: s}
}
func (svc AuthorizedService) FindByID(ctx context.Context, orgID, id influxdb.ID) (*influxdb.DBRPMappingV2, error) {
if _, _, err := authorizer.AuthorizeRead(ctx, influxdb.DBRPResourceType, id, orgID); err != nil {
return nil, err
}
return svc.DBRPMappingServiceV2.FindByID(ctx, orgID, id)
}
func (svc AuthorizedService) FindMany(ctx context.Context, filter influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
dbrps, _, err := svc.DBRPMappingServiceV2.FindMany(ctx, filter, opts...)
if err != nil {
return nil, 0, err
}
return authorizer.AuthorizeFindDBRPs(ctx, dbrps)
}
func (svc AuthorizedService) Create(ctx context.Context, t *influxdb.DBRPMappingV2) error {
if _, _, err := authorizer.AuthorizeCreate(ctx, influxdb.DBRPResourceType, t.OrganizationID); err != nil {
return err
}
return svc.DBRPMappingServiceV2.Create(ctx, t)
}
func (svc AuthorizedService) Update(ctx context.Context, u *influxdb.DBRPMappingV2) error {
if _, _, err := authorizer.AuthorizeWrite(ctx, influxdb.DBRPResourceType, u.ID, u.OrganizationID); err != nil {
return err
}
return svc.DBRPMappingServiceV2.Update(ctx, u)
}
func (svc AuthorizedService) Delete(ctx context.Context, orgID, id influxdb.ID) error {
if _, _, err := authorizer.AuthorizeWrite(ctx, influxdb.DBRPResourceType, id, orgID); err != nil {
return err
}
return svc.DBRPMappingServiceV2.Delete(ctx, orgID, id)
}

View File

@ -0,0 +1,591 @@
package dbrp_test
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/influxdb/v2"
influxdbcontext "github.com/influxdata/influxdb/v2/context"
"github.com/influxdata/influxdb/v2/dbrp"
"github.com/influxdata/influxdb/v2/mock"
influxdbtesting "github.com/influxdata/influxdb/v2/testing"
)
func TestAuth_FindByID(t *testing.T) {
type fields struct {
service influxdb.DBRPMappingServiceV2
}
type args struct {
orgID influxdb.ID
id influxdb.ID
permission influxdb.Permission
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized to access id by org id",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
permission: influxdb.Permission{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
id: 1,
orgID: 1,
},
wants: wants{
err: nil,
},
},
{
name: "authorized to access id by id",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
permission: influxdb.Permission{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
ID: influxdbtesting.IDPtr(1),
},
},
id: 1,
orgID: 1,
},
wants: wants{
err: nil,
},
},
{
name: "unauthorized to access id by org id",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
permission: influxdb.Permission{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
id: 1,
orgID: 2,
},
wants: wants{
err: &influxdb.Error{
Msg: "read:orgs/0000000000000002/dbrp/0000000000000001 is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
{
name: "unauthorized to access id by id",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
permission: influxdb.Permission{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
ID: influxdbtesting.IDPtr(2),
},
},
id: 1,
orgID: 2,
},
wants: wants{
err: &influxdb.Error{
Msg: "read:orgs/0000000000000002/dbrp/0000000000000001 is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := dbrp.NewAuthorizedService(tt.fields.service)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{tt.args.permission}))
_, err := s.FindByID(ctx, tt.args.orgID, tt.args.id)
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}
func TestAuth_FindMany(t *testing.T) {
type fields struct {
service influxdb.DBRPMappingServiceV2
}
type args struct {
filter influxdb.DBRPMappingFilterV2
permissions []influxdb.Permission
}
type wants struct {
err error
ms []*influxdb.DBRPMappingV2
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "no result",
fields: fields{
service: &mock.DBRPMappingServiceV2{
FindManyFn: func(ctx context.Context, dbrp influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
return []*influxdb.DBRPMappingV2{
{
ID: 1,
OrganizationID: 1,
BucketID: 1,
},
{
ID: 2,
OrganizationID: 1,
BucketID: 2,
},
{
ID: 3,
OrganizationID: 2,
BucketID: 3,
},
{
ID: 4,
OrganizationID: 3,
BucketID: 4,
},
}, 4, nil
},
},
},
args: args{
permissions: []influxdb.Permission{{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(42),
},
}},
filter: influxdb.DBRPMappingFilterV2{},
},
wants: wants{
err: nil,
ms: []*influxdb.DBRPMappingV2{},
},
},
{
name: "partial",
fields: fields{
service: &mock.DBRPMappingServiceV2{
FindManyFn: func(ctx context.Context, dbrp influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
return []*influxdb.DBRPMappingV2{
{
ID: 1,
OrganizationID: 1,
BucketID: 1,
},
{
ID: 2,
OrganizationID: 1,
BucketID: 2,
},
{
ID: 3,
OrganizationID: 2,
BucketID: 3,
},
{
ID: 4,
OrganizationID: 3,
BucketID: 4,
},
}, 4, nil
},
},
},
args: args{
permissions: []influxdb.Permission{{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
}},
filter: influxdb.DBRPMappingFilterV2{},
},
wants: wants{
err: nil,
ms: []*influxdb.DBRPMappingV2{
{
ID: 1,
OrganizationID: 1,
BucketID: 1,
},
{
ID: 2,
OrganizationID: 1,
BucketID: 2,
},
},
},
},
{
name: "all",
fields: fields{
service: &mock.DBRPMappingServiceV2{
FindManyFn: func(ctx context.Context, dbrp influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
return []*influxdb.DBRPMappingV2{
{
ID: 1,
OrganizationID: 1,
BucketID: 1,
},
{
ID: 2,
OrganizationID: 1,
BucketID: 2,
},
{
ID: 3,
OrganizationID: 2,
BucketID: 3,
},
{
ID: 4,
OrganizationID: 3,
BucketID: 4,
},
}, 4, nil
},
},
},
args: args{
permissions: []influxdb.Permission{
{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(2),
},
},
{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(3),
},
},
},
filter: influxdb.DBRPMappingFilterV2{},
},
wants: wants{
err: nil,
ms: []*influxdb.DBRPMappingV2{
{
ID: 1,
OrganizationID: 1,
BucketID: 1,
},
{
ID: 2,
OrganizationID: 1,
BucketID: 2,
},
{
ID: 3,
OrganizationID: 2,
BucketID: 3,
},
{
ID: 4,
OrganizationID: 3,
BucketID: 4,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := dbrp.NewAuthorizedService(tt.fields.service)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, tt.args.permissions))
gots, ngots, err := s.FindMany(ctx, tt.args.filter)
if ngots != len(gots) {
t.Errorf("got wrong number back")
}
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
if diff := cmp.Diff(tt.wants.ms, gots, influxdbtesting.DBRPMappingCmpOptionsV2...); diff != "" {
t.Errorf("unexpected result -want/+got:\n\t%s", diff)
}
})
}
}
func TestAuth_Create(t *testing.T) {
type fields struct {
service influxdb.DBRPMappingServiceV2
}
type args struct {
m influxdb.DBRPMappingV2
permission influxdb.Permission
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
m: influxdb.DBRPMappingV2{
ID: 1,
OrganizationID: 1,
},
permission: influxdb.Permission{
Action: "write",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
},
wants: wants{
err: nil,
},
},
{
name: "unauthorized",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
m: influxdb.DBRPMappingV2{
ID: 1,
OrganizationID: 1,
},
permission: influxdb.Permission{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
},
wants: wants{
err: &influxdb.Error{
Msg: "write:orgs/0000000000000001/dbrp is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := dbrp.NewAuthorizedService(tt.fields.service)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{tt.args.permission}))
err := s.Create(ctx, &tt.args.m)
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}
func TestAuth_Update(t *testing.T) {
type fields struct {
service influxdb.DBRPMappingServiceV2
}
type args struct {
orgID influxdb.ID
id influxdb.ID
permission influxdb.Permission
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
permission: influxdb.Permission{
Action: "write",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
id: 1,
orgID: 1,
},
wants: wants{
err: nil,
},
},
{
name: "unauthorized",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
permission: influxdb.Permission{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
id: 1,
orgID: 1,
},
wants: wants{
err: &influxdb.Error{
Msg: "write:orgs/0000000000000001/dbrp/0000000000000001 is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := dbrp.NewAuthorizedService(tt.fields.service)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{tt.args.permission}))
// Does not matter how we update, we only need to check auth.
err := s.Update(ctx, &influxdb.DBRPMappingV2{ID: tt.args.id, OrganizationID: tt.args.orgID, BucketID: 1})
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}
func TestAuth_Delete(t *testing.T) {
type fields struct {
service influxdb.DBRPMappingServiceV2
}
type args struct {
orgID influxdb.ID
id influxdb.ID
permission influxdb.Permission
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "authorized",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
permission: influxdb.Permission{
Action: "write",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
id: 1,
orgID: 1,
},
wants: wants{
err: nil,
},
},
{
name: "unauthorized",
fields: fields{
service: &mock.DBRPMappingServiceV2{},
},
args: args{
permission: influxdb.Permission{
Action: "read",
Resource: influxdb.Resource{
Type: influxdb.DBRPResourceType,
OrgID: influxdbtesting.IDPtr(1),
},
},
id: 1,
orgID: 1,
},
wants: wants{
err: &influxdb.Error{
Msg: "write:orgs/0000000000000001/dbrp/0000000000000001 is unauthorized",
Code: influxdb.EUnauthorized,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := dbrp.NewAuthorizedService(tt.fields.service)
ctx := context.Background()
ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{tt.args.permission}))
err := s.Delete(ctx, tt.args.orgID, tt.args.id)
influxdbtesting.ErrorsEqual(t, err, tt.wants.err)
})
}
}

514
dbrp/service.go Normal file
View File

@ -0,0 +1,514 @@
package dbrp
// The DBRP Mapping `Service` maps database, retention policy pairs to buckets.
// Every `DBRPMapping` stored is scoped to an organization ID.
// The service must ensure the following invariants are valid at any time:
// - each orgID, database, retention policy triple must be unique;
// - for each orgID and database there must exist one and only one default mapping (`mapping.Default` set to `true`).
// The service does so using three kv buckets:
// - one for storing mappings;
// - one for storing an index of mappings by orgID and database;
// - one for storing the current default mapping for an orgID and a database.
//
// On *create*, the service creates the mapping.
// If another mapping with the same orgID, database, and retention policy exists, it fails.
// If the mapping is the first one for the specified orgID-database couple, it will be the default one.
//
// On *find*, the service find mappings.
// Every mapping returned uses the kv bucket where the default is specified to update the `mapping.Default` field.
//
// On *update*, the service updates the mapping.
// If the update causes another bucket to have the same orgID, database, and retention policy, it fails.
// If the update unsets `mapping.Default`, the first mapping found is set as default.
//
// On *delete*, the service updates the mapping.
// If the deletion deletes the default mapping, the first mapping found is set as default.
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/snowflake"
)
var (
bucket = []byte("dbrpv1")
indexBucket = []byte("dbrpbyorganddbindexv1")
defaultBucket = []byte("dbrpdefaultv1")
)
var _ influxdb.DBRPMappingServiceV2 = (*AuthorizedService)(nil)
type Service struct {
store kv.Store
IDGen influxdb.IDGenerator
bucketSvc influxdb.BucketService
byOrgAndDatabase *kv.Index
}
func indexForeignKey(dbrp influxdb.DBRPMappingV2) []byte {
return composeForeignKey(dbrp.OrganizationID, dbrp.Database)
}
func composeForeignKey(orgID influxdb.ID, db string) []byte {
encID, _ := orgID.Encode()
key := make([]byte, len(encID)+len(db))
copy(key, encID)
copy(key[len(encID):], db)
return key
}
func NewService(ctx context.Context, bucketSvc influxdb.BucketService, st kv.Store) (influxdb.DBRPMappingServiceV2, error) {
if err := st.Update(ctx, func(tx kv.Tx) error {
_, err := tx.Bucket(bucket)
if err != nil {
return err
}
_, err = tx.Bucket(indexBucket)
if err != nil {
return err
}
_, err = tx.Bucket(defaultBucket)
return err
}); err != nil {
return nil, err
}
return &Service{
store: st,
IDGen: snowflake.NewDefaultIDGenerator(),
bucketSvc: bucketSvc,
byOrgAndDatabase: kv.NewIndex(kv.NewIndexMapping(bucket, indexBucket, func(v []byte) ([]byte, error) {
var dbrp influxdb.DBRPMappingV2
if err := json.Unmarshal(v, &dbrp); err != nil {
return nil, err
}
return indexForeignKey(dbrp), nil
}), kv.WithIndexReadPathEnabled),
}, nil
}
// getDefault gets the default mapping ID inside of a transaction.
func (s *Service) getDefault(tx kv.Tx, compKey []byte) ([]byte, error) {
b, err := tx.Bucket(defaultBucket)
if err != nil {
return nil, err
}
defID, err := b.Get(compKey)
if err != nil {
return nil, err
}
return defID, nil
}
// getDefaultID returns the default mapping ID for the given orgID and db.
func (s *Service) getDefaultID(tx kv.Tx, compKey []byte) (influxdb.ID, error) {
defID, err := s.getDefault(tx, compKey)
if err != nil {
return 0, err
}
id := new(influxdb.ID)
if err := id.Decode(defID); err != nil {
return 0, err
}
return *id, nil
}
// isDefault tells whether a mapping is the default one.
func (s *Service) isDefault(tx kv.Tx, compKey []byte, id []byte) (bool, error) {
defID, err := s.getDefault(tx, compKey)
if kv.IsNotFound(err) {
return false, nil
}
if err != nil {
return false, err
}
return bytes.Equal(id, defID), nil
}
// isDefaultSet tells if there is a default mapping for the given composite key.
func (s *Service) isDefaultSet(tx kv.Tx, compKey []byte) (bool, error) {
b, err := tx.Bucket(defaultBucket)
if err != nil {
return false, ErrInternalService(err)
}
_, err = b.Get(compKey)
if kv.IsNotFound(err) {
return false, nil
}
if err != nil {
return false, ErrInternalService(err)
}
return true, nil
}
// setAsDefault sets the given id as default for the given composite key.
func (s *Service) setAsDefault(tx kv.Tx, compKey []byte, id []byte) error {
b, err := tx.Bucket(defaultBucket)
if err != nil {
return ErrInternalService(err)
}
if err := b.Put(compKey, id); err != nil {
return ErrInternalService(err)
}
return nil
}
// unsetDefault un-sets the default for the given composite key.
// Useful when a db/rp pair does not exist anymore.
func (s *Service) unsetDefault(tx kv.Tx, compKey []byte) error {
b, err := tx.Bucket(defaultBucket)
if err != nil {
return ErrInternalService(err)
}
if err = b.Delete(compKey); err != nil {
return ErrInternalService(err)
}
return nil
}
// getFirstBut returns the first element in the db/rp index (not accounting for the `skipID`).
// If the length of the returned ID is 0, it means no element was found.
// The skip value is useful, for instance, if one wants to delete an element based on the result of this operation.
func (s *Service) getFirstBut(tx kv.Tx, compKey []byte, skipID []byte) ([]byte, error) {
stop := fmt.Errorf("stop")
var next []byte
if err := s.byOrgAndDatabase.Walk(context.Background(), tx, compKey, func(k, v []byte) error {
if bytes.Equal(skipID, k) {
return nil
}
next = k
return stop
}); err != nil && err != stop {
return nil, ErrInternalService(err)
}
return next, nil
}
// isDBRPUnique verifies if the triple orgID-database-retention-policy is unique.
func (s *Service) isDBRPUnique(ctx context.Context, m influxdb.DBRPMappingV2) error {
return s.store.View(ctx, func(tx kv.Tx) error {
return s.byOrgAndDatabase.Walk(ctx, tx, composeForeignKey(m.OrganizationID, m.Database), func(k, v []byte) error {
dbrp := &influxdb.DBRPMappingV2{}
if err := json.Unmarshal(v, dbrp); err != nil {
return ErrInternalService(err)
}
if dbrp.ID == m.ID {
// Corner case.
// This is the very same DBRP, just skip it!
return nil
}
if dbrp.RetentionPolicy == m.RetentionPolicy {
return ErrDBRPAlreadyExists("another DBRP mapping with same orgID, db, and rp exists")
}
return nil
})
})
}
// FindBy returns the mapping for the given ID.
func (s *Service) FindByID(ctx context.Context, orgID, id influxdb.ID) (*influxdb.DBRPMappingV2, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, ErrInvalidDBRPID
}
m := &influxdb.DBRPMappingV2{}
if err := s.store.View(ctx, func(tx kv.Tx) error {
bucket, err := tx.Bucket(bucket)
if err != nil {
return ErrInternalService(err)
}
b, err := bucket.Get(encodedID)
if err != nil {
return ErrDBRPNotFound
}
if err := json.Unmarshal(b, m); err != nil {
return ErrInternalService(err)
}
// If the given orgID is wrong, it is as if we did not found a mapping scoped to this org.
if m.OrganizationID != orgID {
return ErrDBRPNotFound
}
// Update the default value for this mapping.
m.Default, err = s.isDefault(tx, indexForeignKey(*m), encodedID)
if err != nil {
return ErrInternalService(err)
}
return nil
}); err != nil {
return nil, err
}
return m, nil
}
// FindMany returns a list of mappings that match filter and the total count of matching dbrp mappings.
// TODO(affo): find a smart way to apply FindOptions to a list of items.
func (s *Service) FindMany(ctx context.Context, filter influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
// Memoize default IDs.
defs := make(map[string]*influxdb.ID)
get := func(tx kv.Tx, orgID influxdb.ID, db string) (*influxdb.ID, error) {
k := orgID.String() + db
if _, ok := defs[k]; !ok {
id, err := s.getDefaultID(tx, composeForeignKey(orgID, db))
if kv.IsNotFound(err) {
// Still need to store a not-found result.
defs[k] = nil
} else if err != nil {
return nil, err
} else {
defs[k] = &id
}
}
return defs[k], nil
}
ms := []*influxdb.DBRPMappingV2{}
add := func(tx kv.Tx) func(k, v []byte) error {
return func(k, v []byte) error {
m := influxdb.DBRPMappingV2{}
if err := json.Unmarshal(v, &m); err != nil {
return ErrInternalService(err)
}
// Updating the Default field must be done before filtering.
defID, err := get(tx, m.OrganizationID, m.Database)
if err != nil {
return ErrInternalService(err)
}
m.Default = m.ID == *defID
if filterFunc(&m, filter) {
ms = append(ms, &m)
}
return nil
}
}
return ms, len(ms), s.store.View(ctx, func(tx kv.Tx) error {
// Optimized path, use index.
if orgID := filter.OrgID; orgID != nil {
// The index performs a prefix search.
// The foreign key is `orgID + db`.
// If you want to look by orgID only, just pass orgID as prefix.
db := ""
if filter.Database != nil {
db = *filter.Database
}
compKey := composeForeignKey(*orgID, db)
if len(db) > 0 {
// Even more optimized, looking for the default given an orgID and database.
// No walking index needed.
if def := filter.Default; def != nil && *def {
defID, err := s.getDefault(tx, compKey)
if kv.IsNotFound(err) {
return nil
}
if err != nil {
return ErrInternalService(err)
}
bucket, err := tx.Bucket(bucket)
if err != nil {
return ErrInternalService(err)
}
v, err := bucket.Get(defID)
if err != nil {
return ErrInternalService(err)
}
return add(tx)(defID, v)
}
}
return s.byOrgAndDatabase.Walk(ctx, tx, compKey, add(tx))
}
bucket, err := tx.Bucket(bucket)
if err != nil {
return ErrInternalService(err)
}
cur, err := bucket.Cursor()
if err != nil {
return ErrInternalService(err)
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
if err := add(tx)(k, v); err != nil {
return err
}
}
return nil
})
}
// Create creates a new mapping.
// If another mapping with same organization ID, database, and retention policy exists, an error is returned.
// If the mapping already contains a valid ID, that one is used for storing the mapping.
func (s *Service) Create(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
if !dbrp.ID.Valid() {
dbrp.ID = s.IDGen.ID()
}
if err := dbrp.Validate(); err != nil {
return ErrInvalidDBRP(err)
}
if _, err := s.bucketSvc.FindBucketByID(ctx, dbrp.BucketID); err != nil {
return err
}
// If a dbrp with this particular ID already exists an error is returned.
if _, err := s.FindByID(ctx, dbrp.OrganizationID, dbrp.ID); err == nil {
return ErrDBRPAlreadyExists("dbrp already exist for this particular ID. If you are trying an update use the right function .Update")
}
// If a dbrp with this orgID, db, and rp exists an error is returned.
if err := s.isDBRPUnique(ctx, *dbrp); err != nil {
return err
}
encodedID, err := dbrp.ID.Encode()
if err != nil {
return ErrInvalidDBRPID
}
b, err := json.Marshal(dbrp)
if err != nil {
return ErrInternalService(err)
}
return s.store.Update(ctx, func(tx kv.Tx) error {
bucket, err := tx.Bucket(bucket)
if err != nil {
return ErrInternalService(err)
}
if err := bucket.Put(encodedID, b); err != nil {
return ErrInternalService(err)
}
compKey := indexForeignKey(*dbrp)
if err := s.byOrgAndDatabase.Insert(tx, compKey, encodedID); err != nil {
return err
}
defSet, err := s.isDefaultSet(tx, compKey)
if err != nil {
return err
}
if !defSet {
dbrp.Default = true
}
if dbrp.Default {
if err := s.setAsDefault(tx, compKey, encodedID); err != nil {
return err
}
}
return nil
})
}
// Updates a mapping.
// If another mapping with same organization ID, database, and retention policy exists, an error is returned.
// Un-setting `Default` for a mapping will cause the first one to become the default.
func (s *Service) Update(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
if err := dbrp.Validate(); err != nil {
return ErrInvalidDBRP(err)
}
oldDBRP, err := s.FindByID(ctx, dbrp.OrganizationID, dbrp.ID)
if err != nil {
return ErrDBRPNotFound
}
// Overwrite fields that cannot change.
dbrp.ID = oldDBRP.ID
dbrp.OrganizationID = oldDBRP.OrganizationID
dbrp.BucketID = oldDBRP.BucketID
dbrp.Database = oldDBRP.Database
// If a dbrp with this orgID, db, and rp exists an error is returned.
if err := s.isDBRPUnique(ctx, *dbrp); err != nil {
return err
}
encodedID, err := dbrp.ID.Encode()
if err != nil {
return ErrInternalService(err)
}
b, err := json.Marshal(dbrp)
if err != nil {
return ErrInternalService(err)
}
return s.store.Update(ctx, func(tx kv.Tx) error {
bucket, err := tx.Bucket(bucket)
if err != nil {
return ErrInternalService(err)
}
if err := bucket.Put(encodedID, b); err != nil {
return err
}
compKey := indexForeignKey(*dbrp)
if dbrp.Default {
err = s.setAsDefault(tx, compKey, encodedID)
} else if oldDBRP.Default {
// This means default was unset.
// Need to find a new default.
first, ferr := s.getFirstBut(tx, compKey, encodedID)
if ferr != nil {
return ferr
}
if len(first) > 0 {
err = s.setAsDefault(tx, compKey, first)
}
// If no first was found, then this will remain the default.
}
return err
})
}
// Delete removes a mapping.
// Deleting a mapping that does not exists is not an error.
// Deleting the default mapping will cause the first one (if any) to become the default.
func (s *Service) Delete(ctx context.Context, orgID, id influxdb.ID) error {
dbrp, err := s.FindByID(ctx, orgID, id)
if err != nil {
return nil
}
encodedID, err := id.Encode()
if err != nil {
return ErrInternalService(err)
}
return s.store.Update(ctx, func(tx kv.Tx) error {
bucket, err := tx.Bucket(bucket)
if err != nil {
return ErrInternalService(err)
}
compKey := indexForeignKey(*dbrp)
if err := bucket.Delete(encodedID); err != nil {
return err
}
if err := s.byOrgAndDatabase.Delete(tx, compKey, encodedID); err != nil {
return ErrInternalService(err)
}
// If this was the default, we need to set a new default.
var derr error
if dbrp.Default {
first, err := s.getFirstBut(tx, compKey, encodedID)
if err != nil {
return err
}
if len(first) > 0 {
derr = s.setAsDefault(tx, compKey, first)
} else {
// This means no other mapping is in the index.
// Unset the default
derr = s.unsetDefault(tx, compKey)
}
}
return derr
})
}
// filterFunc is capable to validate if the dbrp is valid from a given filter.
// it runs true if the filtering data are contained in the dbrp.
func filterFunc(dbrp *influxdb.DBRPMappingV2, filter influxdb.DBRPMappingFilterV2) bool {
return (filter.ID == nil || (*filter.ID) == dbrp.ID) &&
(filter.OrgID == nil || (*filter.OrgID) == dbrp.OrganizationID) &&
(filter.BucketID == nil || (*filter.BucketID) == dbrp.BucketID) &&
(filter.Database == nil || (*filter.Database) == dbrp.Database) &&
(filter.RetentionPolicy == nil || (*filter.RetentionPolicy) == dbrp.RetentionPolicy) &&
(filter.Default == nil || (*filter.Default) == dbrp.Default)
}

80
dbrp/service_test.go Normal file
View File

@ -0,0 +1,80 @@
package dbrp_test
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/bolt"
"github.com/influxdata/influxdb/v2/dbrp"
"github.com/influxdata/influxdb/v2/kv"
"github.com/influxdata/influxdb/v2/mock"
itesting "github.com/influxdata/influxdb/v2/testing"
"go.uber.org/zap/zaptest"
)
func NewTestBoltStore(t *testing.T) (kv.Store, func(), error) {
f, err := ioutil.TempFile("", "influxdata-bolt-")
if err != nil {
return nil, nil, errors.New("unable to open temporary boltdb file")
}
f.Close()
path := f.Name()
s := bolt.NewKVStore(zaptest.NewLogger(t), path)
if err := s.Open(context.Background()); err != nil {
return nil, nil, err
}
close := func() {
s.Close()
os.Remove(path)
}
return s, close, nil
}
func initDBRPMappingService(f itesting.DBRPMappingFieldsV2, t *testing.T) (influxdb.DBRPMappingServiceV2, func()) {
s, closeStore, err := NewTestBoltStore(t)
if err != nil {
t.Fatalf("failed to create new bolt kv store: %v", err)
}
ks := kv.NewService(zaptest.NewLogger(t), s)
if err := ks.Initialize(context.Background()); err != nil {
t.Fatal(err)
}
if f.BucketSvc == nil {
f.BucketSvc = &mock.BucketService{
FindBucketByIDFn: func(ctx context.Context, id influxdb.ID) (*influxdb.Bucket, error) {
// always find a bucket.
return &influxdb.Bucket{
ID: id,
Name: fmt.Sprintf("bucket-%v", id),
}, nil
},
}
}
svc, err := dbrp.NewService(context.Background(), f.BucketSvc, s)
if err != nil {
t.Fatal(err)
}
if err := f.Populate(context.Background(), svc); err != nil {
t.Fatal(err)
}
return svc, func() {
if err := itesting.CleanupDBRPMappingsV2(context.Background(), svc); err != nil {
t.Error(err)
}
closeStore()
}
}
func TestBoltDBRPMappingServiceV2(t *testing.T) {
t.Parallel()
itesting.DBRPMappingServiceV2(initDBRPMappingService, t)
}

View File

@ -7,6 +7,145 @@ import (
"unicode"
)
// DBRPMappingServiceV2 provides CRUD to DBRPMappingV2s.
type DBRPMappingServiceV2 interface {
// FindBy returns the dbrp mapping for the specified ID.
// Requires orgID because every resource will be org-scoped.
FindByID(ctx context.Context, orgID, id ID) (*DBRPMappingV2, error)
// FindMany returns a list of dbrp mappings that match filter and the total count of matching dbrp mappings.
FindMany(ctx context.Context, dbrp DBRPMappingFilterV2, opts ...FindOptions) ([]*DBRPMappingV2, int, error)
// Create creates a new dbrp mapping, if a different mapping exists an error is returned.
Create(ctx context.Context, dbrp *DBRPMappingV2) error
// Update a new dbrp mapping
Update(ctx context.Context, dbrp *DBRPMappingV2) error
// Delete removes a dbrp mapping.
// Deleting a mapping that does not exists is not an error.
// Requires orgID because every resource will be org-scoped.
Delete(ctx context.Context, orgID, id ID) error
}
// DBRPMappingV2 represents a mapping of a database and retention policy to an organization ID and bucket ID.
type DBRPMappingV2 struct {
ID ID `json:"id"`
Database string `json:"database"`
RetentionPolicy string `json:"retention_policy"`
// Default indicates if this mapping is the default for the cluster and database.
Default bool `json:"default"`
OrganizationID ID `json:"organization_id"`
BucketID ID `json:"bucket_id"`
}
// Validate reports any validation errors for the mapping.
func (m DBRPMappingV2) Validate() error {
if !validName(m.Database) {
return &Error{
Code: EInvalid,
Msg: "database must contain at least one character and only be letters, numbers, '_', '-', and '.'",
}
}
if !validName(m.RetentionPolicy) {
return &Error{
Code: EInvalid,
Msg: "retentionPolicy must contain at least one character and only be letters, numbers, '_', '-', and '.'",
}
}
if !m.OrganizationID.Valid() {
return &Error{
Code: EInvalid,
Msg: "organizationID is required",
}
}
if !m.BucketID.Valid() {
return &Error{
Code: EInvalid,
Msg: "bucketID is required",
}
}
return nil
}
// Equal checks if the two mappings are identical.
func (m *DBRPMappingV2) Equal(o *DBRPMappingV2) bool {
if m == o {
return true
}
if m == nil || o == nil {
return false
}
return m.Database == o.Database &&
m.RetentionPolicy == o.RetentionPolicy &&
m.Default == o.Default &&
m.OrganizationID.Valid() &&
o.OrganizationID.Valid() &&
m.BucketID.Valid() &&
o.BucketID.Valid() &&
o.ID.Valid() &&
m.ID == o.ID &&
m.OrganizationID == o.OrganizationID &&
m.BucketID == o.BucketID
}
// DBRPMappingFilterV2 represents a set of filters that restrict the returned results.
type DBRPMappingFilterV2 struct {
ID *ID
OrgID *ID
BucketID *ID
Database *string
RetentionPolicy *string
Default *bool
}
func (f DBRPMappingFilterV2) String() string {
var s strings.Builder
s.WriteString("{ id:")
if f.ID != nil {
s.WriteString(f.ID.String())
} else {
s.WriteString("<nil>")
}
s.WriteString(" org_id:")
if f.ID != nil {
s.WriteString(f.OrgID.String())
} else {
s.WriteString("<nil>")
}
s.WriteString(" bucket_id:")
if f.ID != nil {
s.WriteString(f.OrgID.String())
} else {
s.WriteString("<nil>")
}
s.WriteString(" db:")
if f.Database != nil {
s.WriteString(*f.Database)
} else {
s.WriteString("<nil>")
}
s.WriteString(" rp:")
if f.RetentionPolicy != nil {
s.WriteString(*f.RetentionPolicy)
} else {
s.WriteString("<nil>")
}
s.WriteString(" default:")
if f.Default != nil {
s.WriteString(strconv.FormatBool(*f.Default))
} else {
s.WriteString("<nil>")
}
s.WriteString("}")
return s.String()
}
// DBRPMappingService provides a mapping of cluster, database and retention policy to an organization ID and bucket ID.
type DBRPMappingService interface {
// FindBy returns the dbrp mapping the for cluster, db and rp.

View File

@ -7,6 +7,7 @@ import (
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/authorizer"
"github.com/influxdata/influxdb/v2/chronograf/server"
"github.com/influxdata/influxdb/v2/dbrp"
"github.com/influxdata/influxdb/v2/http/metric"
"github.com/influxdata/influxdb/v2/kit/feature"
"github.com/influxdata/influxdb/v2/kit/prom"
@ -58,6 +59,7 @@ type APIBackend struct {
BackupService influxdb.BackupService
KVBackupService influxdb.KVBackupService
AuthorizationService influxdb.AuthorizationService
DBRPService influxdb.DBRPMappingServiceV2
BucketService influxdb.BucketService
SessionService influxdb.SessionService
UserService influxdb.UserService
@ -214,6 +216,8 @@ func NewAPIHandler(b *APIBackend, opts ...APIHandlerOptFn) *APIHandler {
backupBackend.BackupService = authorizer.NewBackupService(backupBackend.BackupService)
h.Mount(prefixBackup, NewBackupHandler(backupBackend))
h.Mount(dbrp.PrefixDBRP, dbrp.NewHTTPHandler(b.Logger, b.DBRPService))
writeBackend := NewWriteBackend(b.Logger.With(zap.String("handler", "write")), b)
h.Mount(prefixWrite, NewWriteHandler(b.Logger, writeBackend,
WithMaxBatchSizeBytes(b.MaxBatchSizeBytes),

View File

@ -7,6 +7,7 @@ import (
"net/url"
"time"
"github.com/influxdata/influxdb/v2/dbrp"
"github.com/influxdata/influxdb/v2/kit/tracing"
"github.com/influxdata/influxdb/v2/pkg/httpc"
)
@ -58,6 +59,7 @@ type Service struct {
*TelegrafService
*LabelService
*SecretService
DBRPMappingServiceV2 *dbrp.Client
}
// NewService returns a service that is an HTTP client to a remote.
@ -99,6 +101,7 @@ func NewService(httpClient *httpc.Client, addr, token string) (*Service, error)
TelegrafService: NewTelegrafService(httpClient),
LabelService: &LabelService{Client: httpClient},
SecretService: &SecretService{Client: httpClient},
DBRPMappingServiceV2: dbrp.NewClient(httpClient),
}, nil
}

View File

@ -345,6 +345,221 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/dbrps:
get:
operationId: GetDPRPs
tags:
- DBRPs
summary: List all database retention policy mappings
parameters:
- $ref: "#/components/parameters/TraceSpan"
- in: query
name: orgID
required: true
description: Specifies the organization ID to filter on
schema:
type: string
- in: query
name: id
description: Specifies the mapping ID to filter on
schema:
type: string
- in: query
name: bucketID
description: Specifies the bucket ID to filter on
schema:
type: string
- in: query
name: default
description: Specifies filtering on default
schema:
type: boolean
- in: query
name: db
description: Specifies the database to filter on
schema:
type: string
- in: query
name: rp
description: Specifies the retention policy to filter on
schema:
type: string
responses:
"200":
description: A list of all database retention policy mappings
content:
application/json:
schema:
$ref: "#/components/schemas/DBRPs"
"400":
description: if any of the parameter passed is invalid
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: Unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
operationId: PostDBRP
tags:
- DBRPs
summary: Add a database retention policy mapping
parameters:
- $ref: "#/components/parameters/TraceSpan"
requestBody:
description: The database retention policy mapping to add
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DBRP"
responses:
"201":
description: Database retention policy mapping created
content:
application/json:
schema:
$ref: "#/components/schemas/DBRP"
"400":
description: if any of the IDs in the mapping is invalid
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: Unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/dprps/{dbrpID}':
get:
operationId: GetDBRPsID
tags:
- DBRPs
summary: Retrieve a database retention policy mapping
parameters:
- $ref: "#/components/parameters/TraceSpan"
- in: query
name: orgID
required: true
description: Specifies the organization ID of the mapping
schema:
type: string
- in: path
name: dbrpID
schema:
type: string
required: true
description: The database retention policy mapping ID
responses:
"200":
description: The database retention policy requested
content:
application/json:
schema:
$ref: "#/components/schemas/DBRP"
"400":
description: if any of the IDs passed is invalid
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: Unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
operationId: PatchDBRPID
tags:
- DBRPs
summary: Update a database retention policy mapping
requestBody:
description: Database retention policy update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DBRPUpdate"
parameters:
- $ref: "#/components/parameters/TraceSpan"
- in: query
name: orgID
required: true
description: Specifies the organization ID of the mapping
schema:
type: string
- in: path
name: dbrpID
schema:
type: string
required: true
description: The database retention policy mapping.
responses:
"200":
description: An updated mapping
content:
application/json:
schema:
$ref: "#/components/schemas/DBRP"
"404":
description: The mapping was not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"400":
description: if any of the IDs passed is invalid
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: Unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
delete:
operationId: DeleteDBRPID
tags:
- DBRPs
summary: Delete a database retention policy
parameters:
- $ref: "#/components/parameters/TraceSpan"
- in: query
name: orgID
required: true
description: Specifies the organization ID of the mapping
schema:
type: string
- in: path
name: dbrpID
schema:
type: string
required: true
description: The database retention policy mapping
responses:
"204":
description: Delete has been accepted
"400":
description: if any of the IDs passed is invalid
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: Unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/telegraf/plugins:
get:
operationId: GetTelegrafPlugins
@ -11200,6 +11415,54 @@ components:
NotificationEndpointType:
type: string
enum: ['slack', 'pagerduty', 'http']
DBRP:
required:
- orgID
- bucketID
- database
- retention_policy
properties:
id:
type: string
description: the mapping identifier
readOnly: true
orgID:
type: string
description: the organization ID that owns this mapping.
bucketID:
type: string
description: the bucket ID used as target for the translation.
database:
type: string
description: InfluxDB v1 database
retention_policy:
type: string
description: InfluxDB v1 retention policy
default:
type: boolean
description: Specify if this mapping represents the default retention policy for the database specificed.
links:
$ref: "#/components/schemas/Links"
DBRPs:
properties:
notificationEndpoints:
type: array
items:
$ref: "#/components/schemas/DBRP"
links:
$ref: "#/components/schemas/Links"
DBRPUpdate:
properties:
database:
type: string
description: InfluxDB v1 database
retention_policy:
type: string
description: InfluxDB v1 retention policy
default:
type: boolean
links:
$ref: "#/components/schemas/Links"
securitySchemes:
BasicAuth:
type: http

View File

@ -3,46 +3,91 @@ package mock
import (
"context"
platform "github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2"
)
var _ influxdb.DBRPMappingServiceV2 = (*DBRPMappingServiceV2)(nil)
type DBRPMappingServiceV2 struct {
FindByIDFn func(ctx context.Context, orgID, id influxdb.ID) (*influxdb.DBRPMappingV2, error)
FindManyFn func(ctx context.Context, dbrp influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error)
CreateFn func(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error
UpdateFn func(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error
DeleteFn func(ctx context.Context, orgID, id influxdb.ID) error
}
func (s *DBRPMappingServiceV2) FindByID(ctx context.Context, orgID, id influxdb.ID) (*influxdb.DBRPMappingV2, error) {
if s.FindByIDFn == nil {
return nil, nil
}
return s.FindByIDFn(ctx, orgID, id)
}
func (s *DBRPMappingServiceV2) FindMany(ctx context.Context, dbrp influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
if s.FindManyFn == nil {
return nil, 0, nil
}
return s.FindManyFn(ctx, dbrp, opts...)
}
func (s *DBRPMappingServiceV2) Create(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
if s.CreateFn == nil {
return nil
}
return s.CreateFn(ctx, dbrp)
}
func (s *DBRPMappingServiceV2) Update(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
if s.UpdateFn == nil {
return nil
}
return s.UpdateFn(ctx, dbrp)
}
func (s *DBRPMappingServiceV2) Delete(ctx context.Context, orgID, id influxdb.ID) error {
if s.DeleteFn == nil {
return nil
}
return s.DeleteFn(ctx, orgID, id)
}
type DBRPMappingService struct {
FindByFn func(ctx context.Context, cluster string, db string, rp string) (*platform.DBRPMapping, error)
FindFn func(ctx context.Context, filter platform.DBRPMappingFilter) (*platform.DBRPMapping, error)
FindManyFn func(ctx context.Context, filter platform.DBRPMappingFilter, opt ...platform.FindOptions) ([]*platform.DBRPMapping, int, error)
CreateFn func(ctx context.Context, dbrpMap *platform.DBRPMapping) error
FindByFn func(ctx context.Context, cluster string, db string, rp string) (*influxdb.DBRPMapping, error)
FindFn func(ctx context.Context, filter influxdb.DBRPMappingFilter) (*influxdb.DBRPMapping, error)
FindManyFn func(ctx context.Context, filter influxdb.DBRPMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.DBRPMapping, int, error)
CreateFn func(ctx context.Context, dbrpMap *influxdb.DBRPMapping) error
DeleteFn func(ctx context.Context, cluster string, db string, rp string) error
}
func NewDBRPMappingService() *DBRPMappingService {
return &DBRPMappingService{
FindByFn: func(ctx context.Context, cluster string, db string, rp string) (*platform.DBRPMapping, error) {
FindByFn: func(ctx context.Context, cluster string, db string, rp string) (*influxdb.DBRPMapping, error) {
return nil, nil
},
FindFn: func(ctx context.Context, filter platform.DBRPMappingFilter) (*platform.DBRPMapping, error) {
FindFn: func(ctx context.Context, filter influxdb.DBRPMappingFilter) (*influxdb.DBRPMapping, error) {
return nil, nil
},
FindManyFn: func(ctx context.Context, filter platform.DBRPMappingFilter, opt ...platform.FindOptions) ([]*platform.DBRPMapping, int, error) {
FindManyFn: func(ctx context.Context, filter influxdb.DBRPMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.DBRPMapping, int, error) {
return nil, 0, nil
},
CreateFn: func(ctx context.Context, dbrpMap *platform.DBRPMapping) error { return nil },
CreateFn: func(ctx context.Context, dbrpMap *influxdb.DBRPMapping) error { return nil },
DeleteFn: func(ctx context.Context, cluster string, db string, rp string) error { return nil },
}
}
func (s *DBRPMappingService) FindBy(ctx context.Context, cluster string, db string, rp string) (*platform.DBRPMapping, error) {
func (s *DBRPMappingService) FindBy(ctx context.Context, cluster string, db string, rp string) (*influxdb.DBRPMapping, error) {
return s.FindByFn(ctx, cluster, db, rp)
}
func (s *DBRPMappingService) Find(ctx context.Context, filter platform.DBRPMappingFilter) (*platform.DBRPMapping, error) {
func (s *DBRPMappingService) Find(ctx context.Context, filter influxdb.DBRPMappingFilter) (*influxdb.DBRPMapping, error) {
return s.FindFn(ctx, filter)
}
func (s *DBRPMappingService) FindMany(ctx context.Context, filter platform.DBRPMappingFilter, opt ...platform.FindOptions) ([]*platform.DBRPMapping, int, error) {
func (s *DBRPMappingService) FindMany(ctx context.Context, filter influxdb.DBRPMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.DBRPMapping, int, error) {
return s.FindManyFn(ctx, filter, opt...)
}
func (s *DBRPMappingService) Create(ctx context.Context, dbrpMap *platform.DBRPMapping) error {
func (s *DBRPMappingService) Create(ctx context.Context, dbrpMap *influxdb.DBRPMapping) error {
return s.CreateFn(ctx, dbrpMap)
}

1760
testing/dbrp_mapping_v2.go Normal file

File diff suppressed because it is too large Load Diff