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
parent
f646653b1b
commit
1cf64fd721
|
@ -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
|
||||
|
|
5
authz.go
5
authz.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
139
dbrp_mapping.go
139
dbrp_mapping.go
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
263
http/swagger.yml
263
http/swagger.yml
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue