* WIP first-class endpoint for labels

* WIP

* WIP

* add all the boilerplate

* fix boltdb

* fix http label test

* fix test

* WIP

* fix test failures

* reenable all tests

* add failing test for label mappings

* add label mapping bolt bucket

* implement resource -> label mapping fn

* add inmem label mapping

* delete label mappings

* remove unused stuff

* add missing functions

* add POST endpoint for labels

* add GET route for label

* delete label endpoint

* add label patch endpoint

* remove commented code

* add label service to api handler

* update comment

* add FindLabelByID test

* use platform.Error

* change path name

* formatting

* remove label patch from swagger

* avoid potential orphaned mapping bug

* guard against creating label mappings from nonexistent labels

* update swagger

* update swagger

* update swagger

* fix swagger indentation

* update swagger
pull/11301/head
Jade McGough 2019-01-18 11:03:36 -08:00 committed by GitHub
parent 220c66dc9a
commit 8a1d7ba1ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 2223 additions and 775 deletions

View File

@ -602,11 +602,6 @@ func (c *Client) deleteBucket(ctx context.Context, tx *bolt.Tx, id platform.ID)
}
}
if err := c.deleteLabels(ctx, tx, platform.LabelFilter{ResourceID: id}); err != nil {
return &platform.Error{
Err: err,
}
}
return nil
}

View File

@ -823,13 +823,6 @@ func (c *Client) deleteDashboard(ctx context.Context, tx *bolt.Tx, id platform.I
}
}
err = c.deleteLabels(ctx, tx, platform.LabelFilter{ResourceID: id})
if err != nil {
return &platform.Error{
Err: err,
}
}
// TODO(desa): add DeleteKeyValueLog method and use it here.
err = c.deleteUserResourceMappings(ctx, tx, platform.UserResourceMappingFilter{
ResourceID: id,

View File

@ -1,47 +1,87 @@
package bolt
import (
"bytes"
"context"
"encoding/json"
"fmt"
bolt "github.com/coreos/bbolt"
platform "github.com/influxdata/influxdb"
)
var (
labelBucket = []byte("labelsv1")
labelBucket = []byte("labelsv1")
labelMappingBucket = []byte("labelmappingsv1")
)
func (c *Client) initializeLabels(ctx context.Context, tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(labelBucket)); err != nil {
return err
}
if _, err := tx.CreateBucketIfNotExists([]byte(labelMappingBucket)); err != nil {
return err
}
return nil
}
// FindLabelByID finds a label by its ID
func (c *Client) FindLabelByID(ctx context.Context, id platform.ID) (*platform.Label, error) {
var l *platform.Label
err := c.db.View(func(tx *bolt.Tx) error {
label, pe := c.findLabelByID(ctx, tx, id)
if pe != nil {
return pe
}
l = label
return nil
})
if err != nil {
return nil, &platform.Error{
Op: getOp(platform.OpFindLabelByID),
Err: err,
}
}
return l, nil
}
func (c *Client) findLabelByID(ctx context.Context, tx *bolt.Tx, id platform.ID) (*platform.Label, *platform.Error) {
encodedID, err := id.Encode()
if err != nil {
return nil, &platform.Error{
Err: err,
}
}
var l platform.Label
v := tx.Bucket(labelBucket).Get(encodedID)
if len(v) == 0 {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "label not found",
}
}
if err := json.Unmarshal(v, &l); err != nil {
return nil, &platform.Error{
Err: err,
}
}
return &l, nil
}
func filterLabelsFn(filter platform.LabelFilter) func(l *platform.Label) bool {
return func(label *platform.Label) bool {
return (filter.Name == "" || (filter.Name == label.Name)) &&
(!filter.ResourceID.Valid() || (filter.ResourceID == label.ResourceID))
return (filter.Name == "" || (filter.Name == label.Name))
}
}
// func (c *Client) findLabel(ctx context.Context, tx *bolt.Tx, resourceID platform.ID, name string) (*platform.Label, error) {
// key, err := labelKey(&platform.Label{ResourceID: resourceID, Name: name})
// if err != nil {
// return nil, err
// }
//
// l := &platform.Label{}
// v := tx.Bucket(labelBucket).Get(key)
// if err := json.Unmarshal(v, l); err != nil {
// return nil, err
// }
//
// return l, nil
// }
// FindLabels returns a list of labels that match a filter.
func (c *Client) FindLabels(ctx context.Context, filter platform.LabelFilter, opt ...platform.FindOptions) ([]*platform.Label, error) {
ls := []*platform.Label{}
@ -78,42 +118,156 @@ func (c *Client) findLabels(ctx context.Context, tx *bolt.Tx, filter platform.La
return ls, nil
}
func (c *Client) CreateLabel(ctx context.Context, l *platform.Label) error {
return c.db.Update(func(tx *bolt.Tx) error {
return c.createLabel(ctx, tx, l)
})
// func filterMappingsFn(filter platform.LabelMappingFilter) func(m *platform.LabelMapping) bool {
// return func(mapping *platform.LabelMapping) bool {
// return (filter.ResourceID.String() == mapping.ResourceID.String()) &&
// (filter.LabelID == nil || filter.LabelID == mapping.LabelID)
// }
// }
func decodeLabelMappingKey(key []byte) (resourceID platform.ID, labelID platform.ID, err error) {
if len(key) != 2*platform.IDLength {
return 0, 0, &platform.Error{Code: platform.EInvalid, Msg: "malformed label mapping key (please report this error)"}
}
if err := (&resourceID).Decode(key[:platform.IDLength]); err != nil {
return 0, 0, &platform.Error{Code: platform.EInvalid, Msg: "bad resource id", Err: platform.ErrInvalidID}
}
if err := (&labelID).Decode(key[platform.IDLength:]); err != nil {
return 0, 0, &platform.Error{Code: platform.EInvalid, Msg: "bad label id", Err: platform.ErrInvalidID}
}
return resourceID, labelID, nil
}
func (c *Client) createLabel(ctx context.Context, tx *bolt.Tx, l *platform.Label) error {
unique := c.uniqueLabel(ctx, tx, l)
func (c *Client) FindResourceLabels(ctx context.Context, filter platform.LabelMappingFilter) ([]*platform.Label, error) {
if !filter.ResourceID.Valid() {
return nil, &platform.Error{Code: platform.EInvalid, Msg: "filter requires a valid resource id", Err: platform.ErrInvalidID}
}
if !unique {
return &platform.Error{
Code: platform.EConflict,
Op: getOp(platform.OpCreateLabel),
Msg: fmt.Sprintf("label %s already exists", l.Name),
ls := []*platform.Label{}
err := c.db.View(func(tx *bolt.Tx) error {
cur := tx.Bucket(labelMappingBucket).Cursor()
prefix, err := filter.ResourceID.Encode()
if err != nil {
return err
}
for k, _ := cur.Seek(prefix); bytes.HasPrefix(k, prefix); k, _ = cur.Next() {
_, id, err := decodeLabelMappingKey(k)
if err != nil {
return err
}
l, err := c.findLabelByID(ctx, tx, id)
if l == nil && err != nil {
// TODO(jm): return error instead of continuing once orphaned mappings are fixed
// (see https://github.com/influxdata/influxdb/issues/11278)
continue
}
ls = append(ls, l)
}
return nil
})
if err != nil {
return nil, &platform.Error{
Err: err,
Op: getOp(platform.OpFindLabelMapping),
}
}
v, err := json.Marshal(l)
return ls, nil
}
// CreateLabelMapping creates a new mapping between a resource and a label.
func (c *Client) CreateLabelMapping(ctx context.Context, m *platform.LabelMapping) error {
_, err := c.FindLabelByID(ctx, *m.LabelID)
if err != nil {
return err
return &platform.Error{
Err: err,
Op: getOp(platform.OpCreateLabel),
}
}
key, err := labelKey(l)
if err != nil {
return err
}
err = c.db.Update(func(tx *bolt.Tx) error {
return c.putLabelMapping(ctx, tx, m)
})
if err := tx.Bucket(labelBucket).Put(key, v); err != nil {
return err
if err != nil {
return &platform.Error{
Err: err,
Op: getOp(platform.OpCreateLabel),
}
}
return nil
}
func labelKey(l *platform.Label) ([]byte, error) {
encodedResourceID, err := l.ResourceID.Encode()
// DeleteLabelMapping deletes a label mapping.
func (c *Client) DeleteLabelMapping(ctx context.Context, m *platform.LabelMapping) error {
err := c.db.Update(func(tx *bolt.Tx) error {
return c.deleteLabelMapping(ctx, tx, m)
})
if err != nil {
return &platform.Error{
Op: getOp(platform.OpDeleteLabelMapping),
Err: err,
}
}
return nil
}
func (c *Client) deleteLabelMapping(ctx context.Context, tx *bolt.Tx, m *platform.LabelMapping) error {
key, err := labelMappingKey(m)
if err != nil {
return &platform.Error{
Err: err,
}
}
if err := tx.Bucket(labelMappingBucket).Delete(key); err != nil {
return &platform.Error{
Err: err,
}
}
return nil
}
// CreateLabel creates a new label.
func (c *Client) CreateLabel(ctx context.Context, l *platform.Label) error {
err := c.db.Update(func(tx *bolt.Tx) error {
l.ID = c.IDGenerator.ID()
return c.putLabel(ctx, tx, l)
})
if err != nil {
return &platform.Error{
Err: err,
Op: getOp(platform.OpCreateLabel),
}
}
return nil
}
// PutLabel creates a label from the provided struct, without generating a new ID.
func (c *Client) PutLabel(ctx context.Context, l *platform.Label) error {
return c.db.Update(func(tx *bolt.Tx) error {
var err error
pe := c.putLabel(ctx, tx, l)
if pe != nil {
err = pe
}
return err
})
}
func labelMappingKey(m *platform.LabelMapping) ([]byte, error) {
lid, err := m.LabelID.Encode()
if err != nil {
return nil, &platform.Error{
Code: platform.EInvalid,
@ -121,9 +275,17 @@ func labelKey(l *platform.Label) ([]byte, error) {
}
}
key := make([]byte, len(encodedResourceID)+len(l.Name))
copy(key, encodedResourceID)
copy(key[len(encodedResourceID):], l.Name)
rid, err := m.ResourceID.Encode()
if err != nil {
return nil, &platform.Error{
Code: platform.EInvalid,
Err: err,
}
}
key := make([]byte, len(rid)+len(lid))
copy(key, rid)
copy(key[len(rid):], lid)
return key, nil
}
@ -143,21 +305,11 @@ func (c *Client) forEachLabel(ctx context.Context, tx *bolt.Tx, fn func(*platfor
return nil
}
func (c *Client) uniqueLabel(ctx context.Context, tx *bolt.Tx, l *platform.Label) bool {
key, err := labelKey(l)
if err != nil {
return false
}
v := tx.Bucket(labelBucket).Get(key)
return len(v) == 0
}
// UpdateLabel updates a label.
func (c *Client) UpdateLabel(ctx context.Context, l *platform.Label, upd platform.LabelUpdate) (*platform.Label, error) {
func (c *Client) UpdateLabel(ctx context.Context, id platform.ID, upd platform.LabelUpdate) (*platform.Label, error) {
var label *platform.Label
err := c.db.Update(func(tx *bolt.Tx) error {
labelResponse, pe := c.updateLabel(ctx, tx, l, upd)
labelResponse, pe := c.updateLabel(ctx, tx, id, upd)
if pe != nil {
return &platform.Error{
Err: pe,
@ -171,19 +323,11 @@ func (c *Client) UpdateLabel(ctx context.Context, l *platform.Label, upd platfor
return label, err
}
func (c *Client) updateLabel(ctx context.Context, tx *bolt.Tx, l *platform.Label, upd platform.LabelUpdate) (*platform.Label, error) {
ls, err := c.findLabels(ctx, tx, platform.LabelFilter{Name: l.Name, ResourceID: l.ResourceID})
func (c *Client) updateLabel(ctx context.Context, tx *bolt.Tx, id platform.ID, upd platform.LabelUpdate) (*platform.Label, error) {
label, err := c.findLabelByID(ctx, tx, id)
if err != nil {
return nil, err
}
if len(ls) == 0 {
return nil, &platform.Error{
Code: platform.ENotFound,
Err: platform.ErrLabelNotFound,
}
}
label := ls[0]
if label.Properties == nil {
label.Properties = make(map[string]string)
@ -222,12 +366,50 @@ func (c *Client) putLabel(ctx context.Context, tx *bolt.Tx, l *platform.Label) e
}
}
key, pe := labelKey(l)
if pe != nil {
return pe
encodedID, err := l.ID.Encode()
if err != nil {
return &platform.Error{
Err: err,
}
}
if err := tx.Bucket(labelBucket).Put(key, v); err != nil {
if err := tx.Bucket(labelBucket).Put(encodedID, v); err != nil {
return &platform.Error{
Err: err,
}
}
return nil
}
// PutLabelMapping writes a label mapping to boltdb
func (c *Client) PutLabelMapping(ctx context.Context, m *platform.LabelMapping) error {
return c.db.Update(func(tx *bolt.Tx) error {
var err error
pe := c.putLabelMapping(ctx, tx, m)
if pe != nil {
err = pe
}
return err
})
}
func (c *Client) putLabelMapping(ctx context.Context, tx *bolt.Tx, m *platform.LabelMapping) error {
v, err := json.Marshal(m)
if err != nil {
return &platform.Error{
Err: err,
}
}
key, err := labelMappingKey(m)
if err != nil {
return &platform.Error{
Err: err,
}
}
if err := tx.Bucket(labelMappingBucket).Put(key, v); err != nil {
return &platform.Error{
Err: err,
}
@ -237,46 +419,50 @@ func (c *Client) putLabel(ctx context.Context, tx *bolt.Tx, l *platform.Label) e
}
// DeleteLabel deletes a label.
func (c *Client) DeleteLabel(ctx context.Context, l platform.Label) error {
return c.db.Update(func(tx *bolt.Tx) error {
return c.deleteLabel(ctx, tx, platform.LabelFilter{Name: l.Name, ResourceID: l.ResourceID})
func (c *Client) DeleteLabel(ctx context.Context, id platform.ID) error {
err := c.db.Update(func(tx *bolt.Tx) error {
return c.deleteLabel(ctx, tx, id)
})
}
func (c *Client) deleteLabel(ctx context.Context, tx *bolt.Tx, filter platform.LabelFilter) error {
ls, err := c.findLabels(ctx, tx, filter)
if err != nil {
return err
}
if len(ls) == 0 {
return &platform.Error{
Code: platform.ENotFound,
Op: getOp(platform.OpDeleteLabel),
Err: platform.ErrLabelNotFound,
}
}
key, err := labelKey(ls[0])
if err != nil {
return err
}
return tx.Bucket(labelBucket).Delete(key)
}
func (c *Client) deleteLabels(ctx context.Context, tx *bolt.Tx, filter platform.LabelFilter) error {
ls, err := c.findLabels(ctx, tx, filter)
if err != nil {
return err
}
for _, l := range ls {
key, err := labelKey(l)
if err != nil {
return err
}
if err = tx.Bucket(labelBucket).Delete(key); err != nil {
return err
Op: getOp(platform.OpDeleteLabel),
Err: err,
}
}
return nil
}
func (c *Client) deleteLabel(ctx context.Context, tx *bolt.Tx, id platform.ID) error {
_, err := c.findLabelByID(ctx, tx, id)
if err != nil {
return err
}
encodedID, idErr := id.Encode()
if idErr != nil {
return &platform.Error{
Err: idErr,
}
}
return tx.Bucket(labelBucket).Delete(encodedID)
}
// func (c *Client) deleteLabels(ctx context.Context, tx *bolt.Tx, filter platform.LabelFilter) error {
// ls, err := c.findLabels(ctx, tx, filter)
// if err != nil {
// return err
// }
// for _, l := range ls {
// encodedID, idErr := l.ID.Encode()
// if idErr != nil {
// return &platform.Error{
// Err: idErr,
// }
// }
//
// if err = tx.Bucket(labelBucket).Delete(encodedID); err != nil {
// return err
// }
// }
// return nil
// }

View File

@ -14,17 +14,25 @@ func initLabelService(f platformtesting.LabelFields, t *testing.T) (platform.Lab
if err != nil {
t.Fatalf("failed to create new bolt client: %v", err)
}
c.IDGenerator = f.IDGenerator
ctx := context.Background()
for _, l := range f.Labels {
if err := c.CreateLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels")
if err := c.PutLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels: %v", err)
}
}
for _, m := range f.Mappings {
if err := c.PutLabelMapping(ctx, m); err != nil {
t.Fatalf("failed to populate label mappings: %v", err)
}
}
return c, bolt.OpPrefix, func() {
defer closeFn()
for _, l := range f.Labels {
if err := c.DeleteLabel(ctx, *l); err != nil {
if err := c.DeleteLabel(ctx, l.ID); err != nil {
t.Logf("failed to remove label: %v", err)
}
}

View File

@ -19,6 +19,7 @@ type APIHandler struct {
OrgHandler *OrgHandler
AuthorizationHandler *AuthorizationHandler
DashboardHandler *DashboardHandler
LabelHandler *LabelHandler
AssetHandler *AssetHandler
ChronografHandler *ChronografHandler
ScraperHandler *ScraperHandler
@ -81,6 +82,9 @@ func NewAPIHandler(b *APIBackend) *APIHandler {
h.BucketHandler.OrganizationService = b.OrganizationService
h.BucketHandler.BucketOperationLogService = b.BucketOperationLogService
h.LabelHandler = NewLabelHandler()
h.LabelHandler.LabelService = b.LabelService
h.OrgHandler = NewOrgHandler(b.UserResourceMappingService, b.LabelService, b.UserService)
h.OrgHandler.OrganizationService = authorizer.NewOrgService(b.OrganizationService)
h.OrgHandler.OrganizationOperationLogService = b.OrganizationOperationLogService
@ -159,6 +163,7 @@ var apiLinks = map[string]interface{}{
"external": map[string]string{
"statusFeed": "https://www.influxdata.com/feed/json",
},
"labels": "/api/v2/labels",
"macros": "/api/v2/macros",
"me": "/api/v2/me",
"orgs": "/api/v2/orgs",
@ -232,6 +237,11 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if strings.HasPrefix(r.URL.Path, "/api/v2/labels") {
h.LabelHandler.ServeHTTP(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/api/v2/users") {
h.UserHandler.ServeHTTP(w, r)
return

View File

@ -31,15 +31,15 @@ type BucketHandler struct {
}
const (
bucketsPath = "/api/v2/buckets"
bucketsIDPath = "/api/v2/buckets/:id"
bucketsIDLogPath = "/api/v2/buckets/:id/log"
bucketsIDMembersPath = "/api/v2/buckets/:id/members"
bucketsIDMembersIDPath = "/api/v2/buckets/:id/members/:userID"
bucketsIDOwnersPath = "/api/v2/buckets/:id/owners"
bucketsIDOwnersIDPath = "/api/v2/buckets/:id/owners/:userID"
bucketsIDLabelsPath = "/api/v2/buckets/:id/labels"
bucketsIDLabelsNamePath = "/api/v2/buckets/:id/labels/:name"
bucketsPath = "/api/v2/buckets"
bucketsIDPath = "/api/v2/buckets/:id"
bucketsIDLogPath = "/api/v2/buckets/:id/log"
bucketsIDMembersPath = "/api/v2/buckets/:id/members"
bucketsIDMembersIDPath = "/api/v2/buckets/:id/members/:userID"
bucketsIDOwnersPath = "/api/v2/buckets/:id/owners"
bucketsIDOwnersIDPath = "/api/v2/buckets/:id/owners/:userID"
bucketsIDLabelsPath = "/api/v2/buckets/:id/labels"
bucketsIDLabelsIDPath = "/api/v2/buckets/:id/labels/:lid"
)
// NewBucketHandler returns a new instance of BucketHandler.
@ -70,8 +70,7 @@ func NewBucketHandler(mappingService platform.UserResourceMappingService, labelS
h.HandlerFunc("GET", bucketsIDLabelsPath, newGetLabelsHandler(h.LabelService))
h.HandlerFunc("POST", bucketsIDLabelsPath, newPostLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", bucketsIDLabelsNamePath, newDeleteLabelHandler(h.LabelService))
h.HandlerFunc("PATCH", bucketsIDLabelsNamePath, newPatchLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", bucketsIDLabelsIDPath, newDeleteLabelHandler(h.LabelService))
return h
}
@ -220,7 +219,7 @@ type bucketsResponse struct {
func newBucketsResponse(ctx context.Context, opts platform.FindOptions, f platform.BucketFilter, bs []*platform.Bucket, labelService platform.LabelService) *bucketsResponse {
rs := make([]*bucketResponse, 0, len(bs))
for _, b := range bs {
labels, _ := labelService.FindLabels(ctx, platform.LabelFilter{ResourceID: b.ID})
labels, _ := labelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: b.ID})
rs = append(rs, newBucketResponse(b, labels))
}
return &bucketsResponse{
@ -305,7 +304,7 @@ func (h *BucketHandler) handleGetBucket(w http.ResponseWriter, r *http.Request)
return
}
labels, err := h.LabelService.FindLabels(ctx, platform.LabelFilter{ResourceID: b.ID})
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: b.ID})
if err != nil {
EncodeError(ctx, err, w)
return
@ -436,7 +435,7 @@ func decodeGetBucketsRequest(ctx context.Context, r *http.Request) (*getBucketsR
return req, nil
}
// handlePatchBucket is the HTTP handler for the PATH /api/v2/buckets route.
// handlePatchBucket is the HTTP handler for the PATCH /api/v2/buckets route.
func (h *BucketHandler) handlePatchBucket(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -452,7 +451,7 @@ func (h *BucketHandler) handlePatchBucket(w http.ResponseWriter, r *http.Request
return
}
labels, err := h.LabelService.FindLabels(ctx, platform.LabelFilter{ResourceID: b.ID})
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: b.ID})
if err != nil {
EncodeError(ctx, err, w)
return

View File

@ -60,11 +60,11 @@ func TestService_handleGetBuckets(t *testing.T) {
},
},
&mock.LabelService{
FindLabelsFn: func(ctx context.Context, f platform.LabelFilter) ([]*platform.Label, error) {
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
labels := []*platform.Label{
{
ResourceID: f.ResourceID,
Name: "label",
ID: platformtesting.MustIDBase16("fc3dc670a4be9b9a"),
Name: "label",
Properties: map[string]string{
"color": "fff000",
},
@ -102,7 +102,7 @@ func TestService_handleGetBuckets(t *testing.T) {
"retentionRules": [{"type": "expire", "everySeconds": 2}],
"labels": [
{
"resourceID": "0b501e7e557ab1ed",
"id": "fc3dc670a4be9b9a",
"name": "label",
"properties": {
"color": "fff000"
@ -123,7 +123,7 @@ func TestService_handleGetBuckets(t *testing.T) {
"retentionRules": [{"type": "expire", "everySeconds": 86400}],
"labels": [
{
"resourceID": "c0175f0077a77005",
"id": "fc3dc670a4be9b9a",
"name": "label",
"properties": {
"color": "fff000"

View File

@ -41,7 +41,7 @@ const (
dashboardsIDOwnersPath = "/api/v2/dashboards/:id/owners"
dashboardsIDOwnersIDPath = "/api/v2/dashboards/:id/owners/:userID"
dashboardsIDLabelsPath = "/api/v2/dashboards/:id/labels"
dashboardsIDLabelsNamePath = "/api/v2/dashboards/:id/labels/:name"
dashboardsIDLabelsIDPath = "/api/v2/dashboards/:id/labels/:lid"
)
// NewDashboardHandler returns a new instance of DashboardHandler.
@ -80,8 +80,7 @@ func NewDashboardHandler(mappingService platform.UserResourceMappingService, lab
h.HandlerFunc("GET", dashboardsIDLabelsPath, newGetLabelsHandler(h.LabelService))
h.HandlerFunc("POST", dashboardsIDLabelsPath, newPostLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", dashboardsIDLabelsNamePath, newDeleteLabelHandler(h.LabelService))
h.HandlerFunc("PATCH", dashboardsIDLabelsNamePath, newPatchLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", dashboardsIDLabelsIDPath, newDeleteLabelHandler(h.LabelService))
return h
}
@ -332,7 +331,7 @@ func newGetDashboardsResponse(ctx context.Context, dashboards []*platform.Dashbo
for _, dashboard := range dashboards {
if dashboard != nil {
labels, _ := labelService.FindLabels(ctx, platform.LabelFilter{ResourceID: dashboard.ID})
labels, _ := labelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: dashboard.ID})
res.Dashboards = append(res.Dashboards, newDashboardResponse(dashboard, labels))
}
}
@ -390,7 +389,7 @@ func (h *DashboardHandler) handleGetDashboard(w http.ResponseWriter, r *http.Req
return
}
labels, err := h.LabelService.FindLabels(ctx, platform.LabelFilter{ResourceID: dashboard.ID})
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: dashboard.ID})
if err != nil {
EncodeError(ctx, err, w)
return
@ -542,7 +541,7 @@ func (h *DashboardHandler) handlePatchDashboard(w http.ResponseWriter, r *http.R
return
}
labels, err := h.LabelService.FindLabels(ctx, platform.LabelFilter{ResourceID: dashboard.ID})
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: dashboard.ID})
if err != nil {
EncodeError(ctx, err, w)
return

View File

@ -76,11 +76,11 @@ func TestService_handleGetDashboards(t *testing.T) {
},
},
&mock.LabelService{
FindLabelsFn: func(ctx context.Context, f platform.LabelFilter) ([]*platform.Label, error) {
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
labels := []*platform.Label{
{
ResourceID: f.ResourceID,
Name: "label",
ID: platformtesting.MustIDBase16("fc3dc670a4be9b9a"),
Name: "label",
Properties: map[string]string{
"color": "fff000",
},
@ -107,7 +107,7 @@ func TestService_handleGetDashboards(t *testing.T) {
"description": "oh hello there!",
"labels": [
{
"resourceID": "da7aba5e5d81e550",
"id": "fc3dc670a4be9b9a",
"name": "label",
"properties": {
"color": "fff000"
@ -148,7 +148,7 @@ func TestService_handleGetDashboards(t *testing.T) {
"description": "",
"labels": [
{
"resourceID": "0ca2204eca2204e0",
"id": "fc3dc670a4be9b9a",
"name": "label",
"properties": {
"color": "fff000"
@ -184,7 +184,7 @@ func TestService_handleGetDashboards(t *testing.T) {
},
},
&mock.LabelService{
FindLabelsFn: func(ctx context.Context, f platform.LabelFilter) ([]*platform.Label, error) {
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
return []*platform.Label{}, nil
},
},
@ -231,11 +231,11 @@ func TestService_handleGetDashboards(t *testing.T) {
},
},
&mock.LabelService{
FindLabelsFn: func(ctx context.Context, f platform.LabelFilter) ([]*platform.Label, error) {
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
labels := []*platform.Label{
{
ResourceID: f.ResourceID,
Name: "label",
ID: platformtesting.MustIDBase16("fc3dc670a4be9b9a"),
Name: "label",
Properties: map[string]string{
"color": "fff000",
},
@ -267,16 +267,16 @@ func TestService_handleGetDashboards(t *testing.T) {
"meta": {
"createdAt": "2009-11-10T23:00:00Z",
"updatedAt": "2009-11-11T00:00:00Z"
},
"labels": [
{
"resourceID": "da7aba5e5d81e550",
"name": "label",
"properties": {
"color": "fff000"
}
}
],
},
"labels": [
{
"id": "fc3dc670a4be9b9a",
"name": "label",
"properties": {
"color": "fff000"
}
}
],
"cells": [
{
"id": "da7aba5e5d81e550",

View File

@ -8,28 +8,268 @@ import (
"net/http"
"path"
plat "github.com/influxdata/influxdb"
platform "github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/kit/errors"
kerrors "github.com/influxdata/influxdb/kit/errors"
"github.com/julienschmidt/httprouter"
"go.uber.org/zap"
)
// LabelHandler represents an HTTP API handler for labels
type LabelHandler struct {
*httprouter.Router
Logger *zap.Logger
LabelService platform.LabelService
}
const (
labelsPath = "/api/v2/labels"
labelsIDPath = "/api/v2/labels/:id"
)
// NewLabelHandler returns a new instance of LabelHandler
func NewLabelHandler() *LabelHandler {
h := &LabelHandler{
Router: NewRouter(),
Logger: zap.NewNop(),
}
h.HandlerFunc("POST", labelsPath, h.handlePostLabel)
h.HandlerFunc("GET", labelsPath, h.handleGetLabels)
h.HandlerFunc("GET", labelsIDPath, h.handleGetLabel)
h.HandlerFunc("PATCH", labelsIDPath, h.handlePatchLabel)
h.HandlerFunc("DELETE", labelsIDPath, h.handleDeleteLabel)
return h
}
// handlePostLabel is the HTTP handler for the POST /api/v2/labels route.
func (h *LabelHandler) handlePostLabel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostLabelRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := h.LabelService.CreateLabel(ctx, req.Label); err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, newLabelResponse(req.Label)); err != nil {
logEncodingError(h.Logger, r, err)
return
}
}
type postLabelRequest struct {
Label *platform.Label
}
func (b postLabelRequest) Validate() error {
if b.Label.Name == "" {
return &platform.Error{
Code: platform.EInvalid,
Msg: "label requires a name",
}
}
return nil
}
func decodePostLabelRequest(ctx context.Context, r *http.Request) (*postLabelRequest, error) {
l := &platform.Label{}
if err := json.NewDecoder(r.Body).Decode(l); err != nil {
return nil, &platform.Error{
Code: platform.EInvalid,
Msg: "unable to decode label request",
Err: err,
}
}
req := &postLabelRequest{
Label: l,
}
return req, req.Validate()
}
// handleGetLabels is the HTTP handler for the GET /api/v2/labels route.
func (h *LabelHandler) handleGetLabels(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
labels, err := h.LabelService.FindLabels(ctx, platform.LabelFilter{})
if err != nil {
EncodeError(ctx, err, w)
return
}
err = encodeResponse(ctx, w, http.StatusOK, newLabelsResponse(labels))
if err != nil {
EncodeError(ctx, err, w)
return
}
}
// handleGetLabel is the HTTP handler for the GET /api/v2/labels/id route.
func (h *LabelHandler) handleGetLabel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetLabelRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
l, err := h.LabelService.FindLabelByID(ctx, req.LabelID)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newLabelResponse(l)); err != nil {
logEncodingError(h.Logger, r, err)
return
}
}
type getLabelRequest struct {
LabelID platform.ID
}
func decodeGetLabelRequest(ctx context.Context, r *http.Request) (*getLabelRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
Msg: "label id is not valid",
}
}
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
req := &getLabelRequest{
LabelID: i,
}
return req, nil
}
// handleDeleteLabel is the HTTP handler for the DELETE /api/v2/labels/:id route.
func (h *LabelHandler) handleDeleteLabel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeDeleteLabelRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := h.LabelService.DeleteLabel(ctx, req.LabelID); err != nil {
EncodeError(ctx, err, w)
return
}
w.WriteHeader(http.StatusNoContent)
}
type deleteLabelRequest struct {
LabelID platform.ID
}
func decodeDeleteLabelRequest(ctx context.Context, r *http.Request) (*deleteLabelRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
req := &deleteLabelRequest{
LabelID: i,
}
return req, nil
}
// handlePatchLabel is the HTTP handler for the PATCH /api/v2/labels route.
func (h *LabelHandler) handlePatchLabel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePatchLabelRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
l, err := h.LabelService.UpdateLabel(ctx, req.LabelID, req.Update)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newLabelResponse(l)); err != nil {
logEncodingError(h.Logger, r, err)
return
}
}
type patchLabelRequest struct {
Update platform.LabelUpdate
LabelID platform.ID
}
func decodePatchLabelRequest(ctx context.Context, r *http.Request) (*patchLabelRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, errors.InvalidDataf("url missing id")
}
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
upd := &platform.LabelUpdate{}
if err := json.NewDecoder(r.Body).Decode(upd); err != nil {
return nil, err
}
return &patchLabelRequest{
Update: *upd,
LabelID: i,
}, nil
}
// LabelService connects to Influx via HTTP using tokens to manage labels
type LabelService struct {
Addr string
Token string
InsecureSkipVerify bool
BasePath string
OpPrefix string
}
type labelResponse struct {
Links map[string]string `json:"links"`
Label plat.Label `json:"label"`
Label platform.Label `json:"label"`
}
// TODO: remove "dashboard" from this
func newLabelResponse(l *plat.Label) *labelResponse {
func newLabelResponse(l *platform.Label) *labelResponse {
return &labelResponse{
Links: map[string]string{
"resource": fmt.Sprintf("/api/v2/%ss/%s", "dashboard", l.ResourceID),
"self": fmt.Sprintf("/api/v2/labels/%s", l.ID),
},
Label: *l,
}
@ -37,21 +277,20 @@ func newLabelResponse(l *plat.Label) *labelResponse {
type labelsResponse struct {
Links map[string]string `json:"links"`
Labels []*plat.Label `json:"labels"`
Labels []*platform.Label `json:"labels"`
}
func newLabelsResponse(opts plat.FindOptions, f plat.LabelFilter, ls []*plat.Label) *labelsResponse {
// TODO: Remove "dashboard" from this
func newLabelsResponse(ls []*platform.Label) *labelsResponse {
return &labelsResponse{
Links: map[string]string{
"resource": fmt.Sprintf("/api/v2/%ss/%s", "dashboard", f.ResourceID),
"self": fmt.Sprintf("/api/v2/labels"),
},
Labels: ls,
}
}
// newGetLabelsHandler returns a handler func for a GET to /labels endpoints
func newGetLabelsHandler(s plat.LabelService) http.HandlerFunc {
func newGetLabelsHandler(s platform.LabelService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -61,14 +300,13 @@ func newGetLabelsHandler(s plat.LabelService) http.HandlerFunc {
return
}
opts := plat.FindOptions{}
labels, err := s.FindLabels(ctx, req.filter)
labels, err := s.FindResourceLabels(ctx, req.filter)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newLabelsResponse(opts, req.filter, labels)); err != nil {
if err := encodeResponse(ctx, w, http.StatusOK, newLabelsResponse(labels)); err != nil {
// TODO: this can potentially result in calling w.WriteHeader multiple times, we need to pass a logger in here
// some how. This isn't as simple as simply passing in a logger to this function since the time that this function
// is called is distinct from the time that a potential logger is set.
@ -79,11 +317,10 @@ func newGetLabelsHandler(s plat.LabelService) http.HandlerFunc {
}
type getLabelsRequest struct {
filter plat.LabelFilter
filter platform.LabelMappingFilter
}
func decodeGetLabelsRequest(ctx context.Context, r *http.Request) (*getLabelsRequest, error) {
qp := r.URL.Query()
req := &getLabelsRequest{}
params := httprouter.ParamsFromContext(ctx)
@ -92,41 +329,43 @@ func decodeGetLabelsRequest(ctx context.Context, r *http.Request) (*getLabelsReq
return nil, kerrors.InvalidDataf("url missing id")
}
var i plat.ID
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
req.filter.ResourceID = i
if name := qp.Get("name"); name != "" {
req.filter.Name = name
}
return req, nil
}
// newPostLabelHandler returns a handler func for a POST to /labels endpoints
func newPostLabelHandler(s plat.LabelService) http.HandlerFunc {
func newPostLabelHandler(s platform.LabelService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostLabelRequest(ctx, r)
req, err := decodePostLabelMappingRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := req.Label.Validate(); err != nil {
if err := req.Mapping.Validate(); err != nil {
EncodeError(ctx, err, w)
return
}
if err := s.CreateLabel(ctx, &req.Label); err != nil {
if err := s.CreateLabelMapping(ctx, &req.Mapping); err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, newLabelResponse(&req.Label)); err != nil {
label, err := s.FindLabelByID(ctx, *req.Mapping.LabelID)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, newLabelResponse(label)); err != nil {
// TODO: this can potentially result in calling w.WriteHeader multiple times, we need to pass a logger in here
// some how. This isn't as simple as simply passing in a logger to this function since the time that this function
// is called is distinct from the time that a potential logger is set.
@ -136,123 +375,57 @@ func newPostLabelHandler(s plat.LabelService) http.HandlerFunc {
}
}
type postLabelRequest struct {
Label plat.Label
type postLabelMappingRequest struct {
Mapping platform.LabelMapping
}
func decodePostLabelRequest(ctx context.Context, r *http.Request) (*postLabelRequest, error) {
func decodePostLabelMappingRequest(ctx context.Context, r *http.Request) (*postLabelMappingRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, kerrors.InvalidDataf("url missing id")
}
var rid plat.ID
var rid platform.ID
if err := rid.DecodeFromString(id); err != nil {
return nil, err
}
label := &plat.Label{}
if err := json.NewDecoder(r.Body).Decode(label); err != nil {
mapping := &platform.LabelMapping{}
if err := json.NewDecoder(r.Body).Decode(mapping); err != nil {
return nil, err
}
label.ResourceID = rid
mapping.ResourceID = &rid
if err := label.Validate(); err != nil {
if err := mapping.Validate(); err != nil {
return nil, err
}
req := &postLabelRequest{
Label: *label,
req := &postLabelMappingRequest{
Mapping: *mapping,
}
return req, nil
}
type patchLabelRequest struct {
label *plat.Label
upd plat.LabelUpdate
}
func decodePatchLabelRequest(ctx context.Context, r *http.Request) (*patchLabelRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &plat.Error{
Code: plat.EInvalid,
Msg: "url missing resource id",
}
}
name := params.ByName("name")
if name == "" {
return nil, &plat.Error{
Code: plat.EInvalid,
Msg: "label name is missing",
}
}
var rid plat.ID
if err := rid.DecodeFromString(id); err != nil {
return nil, err
}
upd := &plat.LabelUpdate{}
if err := json.NewDecoder(r.Body).Decode(upd); err != nil {
return nil, err
}
return &patchLabelRequest{
label: &plat.Label{ResourceID: rid, Name: name},
upd: *upd,
}, nil
}
// newPatchLabelHandler returns a handler func for a PATCH to /labels endpoints
func newPatchLabelHandler(s plat.LabelService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePatchLabelRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
label, err := s.UpdateLabel(ctx, req.label, req.upd)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newLabelResponse(label)); err != nil {
// TODO: this can potentially result in calling w.WriteHeader multiple times, we need to pass a logger in here
// some how. This isn't as simple as simply passing in a logger to this function since the time that this function
// is called is distinct from the time that a potential logger is set.
EncodeError(ctx, err, w)
return
}
}
}
// newDeleteLabelHandler returns a handler func for a DELETE to /labels endpoints
func newDeleteLabelHandler(s plat.LabelService) http.HandlerFunc {
func newDeleteLabelHandler(s platform.LabelService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeDeleteLabelRequest(ctx, r)
req, err := decodeDeleteLabelMappingRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
label := plat.Label{
ResourceID: req.ResourceID,
Name: req.Name,
mapping := &platform.LabelMapping{
LabelID: &req.LabelID,
ResourceID: &req.ResourceID,
}
if err := s.DeleteLabel(ctx, label); err != nil {
if err := s.DeleteLabelMapping(ctx, mapping); err != nil {
EncodeError(ctx, err, w)
return
}
@ -261,42 +434,85 @@ func newDeleteLabelHandler(s plat.LabelService) http.HandlerFunc {
}
}
type deleteLabelRequest struct {
ResourceID plat.ID
Name string
type deleteLabelMappingRequest struct {
ResourceID platform.ID
LabelID platform.ID
}
func decodeDeleteLabelRequest(ctx context.Context, r *http.Request) (*deleteLabelRequest, error) {
func decodeDeleteLabelMappingRequest(ctx context.Context, r *http.Request) (*deleteLabelMappingRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &plat.Error{
Code: plat.EInvalid,
return nil, &platform.Error{
Code: platform.EInvalid,
Msg: "url missing resource id",
}
}
name := params.ByName("name")
if name == "" {
return nil, &plat.Error{
Code: plat.EInvalid,
Msg: "label name is missing",
}
}
var rid plat.ID
var rid platform.ID
if err := rid.DecodeFromString(id); err != nil {
return nil, err
}
return &deleteLabelRequest{
Name: name,
id = params.ByName("lid")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
Msg: "label id is missing",
}
}
var lid platform.ID
if err := lid.DecodeFromString(id); err != nil {
return nil, err
}
return &deleteLabelMappingRequest{
LabelID: lid,
ResourceID: rid,
}, nil
}
// FindLabels returns a slice of labels
func (s *LabelService) FindLabels(ctx context.Context, filter plat.LabelFilter, opt ...plat.FindOptions) ([]*plat.Label, error) {
func labelIDPath(id platform.ID) string {
return path.Join(labelsPath, id.String())
}
// FindLabelByID returns a single label by ID.
func (s *LabelService) FindLabelByID(ctx context.Context, id platform.ID) (*platform.Label, error) {
u, err := newURL(s.Addr, labelIDPath(id))
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
if err := CheckError(resp, true); err != nil {
return nil, err
}
var lr labelResponse
if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
return nil, err
}
return &lr.Label, nil
}
func (s *LabelService) FindLabels(ctx context.Context, filter platform.LabelFilter, opt ...platform.FindOptions) ([]*platform.Label, error) {
return nil, nil
}
// FindResourceLabels returns a list of labels, derived from a label mapping filter.
func (s *LabelService) FindResourceLabels(ctx context.Context, filter platform.LabelMappingFilter) ([]*platform.Label, error) {
url, err := newURL(s.Addr, resourceIDPath(s.BasePath, filter.ResourceID))
if err != nil {
return nil, err
@ -328,12 +544,9 @@ func (s *LabelService) FindLabels(ctx context.Context, filter plat.LabelFilter,
return r.Labels, nil
}
func (s *LabelService) CreateLabel(ctx context.Context, l *plat.Label) error {
if err := l.Validate(); err != nil {
return err
}
url, err := newURL(s.Addr, resourceIDPath(s.BasePath, l.ResourceID))
// CreateLabel creates a new label.
func (s *LabelService) CreateLabel(ctx context.Context, l *platform.Label) error {
u, err := newURL(s.Addr, labelsPath)
if err != nil {
return err
}
@ -343,6 +556,49 @@ func (s *LabelService) CreateLabel(ctx context.Context, l *plat.Label) error {
return err
}
req, err := http.NewRequest("POST", u.String(), bytes.NewReader(octets))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
// TODO(jsternberg): Should this check for a 201 explicitly?
if err := CheckError(resp, true); err != nil {
return err
}
var lr labelResponse
if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
return err
}
return nil
}
func (s *LabelService) CreateLabelMapping(ctx context.Context, m *platform.LabelMapping) error {
if err := m.Validate(); err != nil {
return err
}
url, err := newURL(s.Addr, resourceIDPath(s.BasePath, *m.ResourceID))
if err != nil {
return err
}
octets, err := json.Marshal(m)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url.String(), bytes.NewReader(octets))
if err != nil {
return err
@ -362,15 +618,74 @@ func (s *LabelService) CreateLabel(ctx context.Context, l *plat.Label) error {
return err
}
if err := json.NewDecoder(resp.Body).Decode(l); err != nil {
if err := json.NewDecoder(resp.Body).Decode(m); err != nil {
return err
}
return nil
}
func (s *LabelService) DeleteLabel(ctx context.Context, l plat.Label) error {
url, err := newURL(s.Addr, labelNamePath(s.BasePath, l.ResourceID, l.Name))
// UpdateLabel updates a label and returns the updated label.
func (s *LabelService) UpdateLabel(ctx context.Context, id platform.ID, upd platform.LabelUpdate) (*platform.Label, error) {
u, err := newURL(s.Addr, labelIDPath(id))
if err != nil {
return nil, err
}
octets, err := json.Marshal(upd)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PATCH", u.String(), bytes.NewReader(octets))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
if err := CheckError(resp, true); err != nil {
return nil, err
}
var lr labelResponse
if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
return nil, err
}
return &lr.Label, nil
}
// DeleteLabel removes a label by ID.
func (s *LabelService) DeleteLabel(ctx context.Context, id platform.ID) error {
u, err := newURL(s.Addr, labelIDPath(id))
if err != nil {
return err
}
req, err := http.NewRequest("DELETE", u.String(), nil)
if err != nil {
return err
}
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
return CheckError(resp, true)
}
func (s *LabelService) DeleteLabelMapping(ctx context.Context, m *platform.LabelMapping) error {
url, err := newURL(s.Addr, labelNamePath(s.BasePath, *m.ResourceID, *m.LabelID))
if err != nil {
return err
}
@ -389,6 +704,6 @@ func (s *LabelService) DeleteLabel(ctx context.Context, l plat.Label) error {
return CheckError(resp)
}
func labelNamePath(basePath string, resourceID plat.ID, name string) string {
return path.Join(basePath, resourceID.String(), "labels", name)
func labelNamePath(basePath string, resourceID platform.ID, labelID platform.ID) string {
return path.Join(basePath, resourceID.String(), "labels", labelID.String())
}

582
http/label_test.go Normal file
View File

@ -0,0 +1,582 @@
package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
http "net/http"
"net/http/httptest"
"testing"
platform "github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/mock"
platformtesting "github.com/influxdata/influxdb/testing"
"github.com/julienschmidt/httprouter"
)
func TestService_handleGetLabels(t *testing.T) {
type fields struct {
LabelService platform.LabelService
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
wants wants
}{
{
name: "get all labels",
fields: fields{
&mock.LabelService{
FindLabelsFn: func(ctx context.Context, filter platform.LabelFilter) ([]*platform.Label, error) {
return []*platform.Label{
{
ID: platformtesting.MustIDBase16("0b501e7e557ab1ed"),
Name: "hello",
Properties: map[string]string{
"color": "fff000",
},
},
{
ID: platformtesting.MustIDBase16("c0175f0077a77005"),
Name: "example",
Properties: map[string]string{
"color": "fff000",
},
},
}, nil
},
},
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"self": "/api/v2/labels"
},
"labels": [
{
"id": "0b501e7e557ab1ed",
"name": "hello",
"properties": {
"color": "fff000"
}
},
{
"id": "c0175f0077a77005",
"name": "example",
"properties": {
"color": "fff000"
}
}
]
}
`,
},
},
{
name: "get all labels when there are none",
fields: fields{
&mock.LabelService{
FindLabelsFn: func(ctx context.Context, filter platform.LabelFilter) ([]*platform.Label, error) {
return []*platform.Label{}, nil
},
},
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"self": "/api/v2/labels"
},
"labels": []
}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewLabelHandler()
h.LabelService = tt.fields.LabelService
r := httptest.NewRequest("GET", "http://any.url", nil)
w := httptest.NewRecorder()
h.handleGetLabels(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handleGetLabels() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handleGetLabels() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil || tt.wants.body != "" && !eq {
t.Errorf("%q. handleGetLabels() = ***%v***", tt.name, diff)
}
})
}
}
func TestService_handleGetLabel(t *testing.T) {
type fields struct {
LabelService platform.LabelService
}
type args struct {
id string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get a label by id",
fields: fields{
&mock.LabelService{
FindLabelByIDFn: func(ctx context.Context, id platform.ID) (*platform.Label, error) {
if id == platformtesting.MustIDBase16("020f755c3c082000") {
return &platform.Label{
ID: platformtesting.MustIDBase16("020f755c3c082000"),
Name: "mylabel",
Properties: map[string]string{
"color": "fff000",
},
}, nil
}
return nil, fmt.Errorf("not found")
},
},
},
args: args{
id: "020f755c3c082000",
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"self": "/api/v2/labels/020f755c3c082000"
},
"label": {
"id": "020f755c3c082000",
"name": "mylabel",
"properties": {
"color": "fff000"
}
}
}
`,
},
},
{
name: "not found",
fields: fields{
&mock.LabelService{
FindLabelByIDFn: func(ctx context.Context, id platform.ID) (*platform.Label, error) {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "label not found",
}
},
},
},
args: args{
id: "020f755c3c082000",
},
wants: wants{
statusCode: http.StatusNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewLabelHandler()
h.LabelService = tt.fields.LabelService
r := httptest.NewRequest("GET", "http://any.url", nil)
r = r.WithContext(context.WithValue(
context.Background(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "id",
Value: tt.args.id,
},
}))
w := httptest.NewRecorder()
h.handleGetLabel(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handleGetLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handleGetLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. handleGetLabel() = ***%v***", tt.name, diff)
}
})
}
}
func TestService_handlePostLabel(t *testing.T) {
type fields struct {
LabelService platform.LabelService
}
type args struct {
label *platform.Label
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "create a new label",
fields: fields{
&mock.LabelService{
CreateLabelFn: func(ctx context.Context, l *platform.Label) error {
l.ID = platformtesting.MustIDBase16("020f755c3c082000")
return nil
},
},
},
args: args{
label: &platform.Label{
Name: "mylabel",
},
},
wants: wants{
statusCode: http.StatusCreated,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"self": "/api/v2/labels/020f755c3c082000"
},
"label": {
"id": "020f755c3c082000",
"name": "mylabel"
}
}
`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewLabelHandler()
h.LabelService = tt.fields.LabelService
l, err := json.Marshal(tt.args.label)
if err != nil {
t.Fatalf("failed to marshal label: %v", err)
}
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(l))
w := httptest.NewRecorder()
h.handlePostLabel(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handlePostLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handlePostLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, diff, err := jsonEqual(string(body), tt.wants.body); err != nil || tt.wants.body != "" && !eq {
t.Errorf("%q. handlePostLabel() = ***%v***", tt.name, diff)
}
})
}
}
func TestService_handleDeleteLabel(t *testing.T) {
type fields struct {
LabelService platform.LabelService
}
type args struct {
id string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "remove a label by id",
fields: fields{
&mock.LabelService{
DeleteLabelFn: func(ctx context.Context, id platform.ID) error {
if id == platformtesting.MustIDBase16("020f755c3c082000") {
return nil
}
return fmt.Errorf("wrong id")
},
},
},
args: args{
id: "020f755c3c082000",
},
wants: wants{
statusCode: http.StatusNoContent,
},
},
{
name: "label not found",
fields: fields{
&mock.LabelService{
DeleteLabelFn: func(ctx context.Context, id platform.ID) error {
return &platform.Error{
Code: platform.ENotFound,
Msg: "label not found",
}
},
},
},
args: args{
id: "020f755c3c082000",
},
wants: wants{
statusCode: http.StatusNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewLabelHandler()
h.LabelService = tt.fields.LabelService
r := httptest.NewRequest("GET", "http://any.url", nil)
r = r.WithContext(context.WithValue(
context.Background(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "id",
Value: tt.args.id,
},
}))
w := httptest.NewRecorder()
h.handleDeleteLabel(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handlePostLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handlePostLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. handlePostLabel() = ***%v***", tt.name, diff)
}
})
}
}
func TestService_handlePatchLabel(t *testing.T) {
type fields struct {
LabelService platform.LabelService
}
type args struct {
id string
properties map[string]string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "update label properties",
fields: fields{
&mock.LabelService{
UpdateLabelFn: func(ctx context.Context, id platform.ID, upd platform.LabelUpdate) (*platform.Label, error) {
if id == platformtesting.MustIDBase16("020f755c3c082000") {
l := &platform.Label{
ID: platformtesting.MustIDBase16("020f755c3c082000"),
Name: "mylabel",
Properties: map[string]string{
"color": "fff000",
},
}
for k, v := range upd.Properties {
if v == "" {
delete(l.Properties, k)
} else {
l.Properties[k] = v
}
}
return l, nil
}
return nil, fmt.Errorf("not found")
},
},
},
args: args{
id: "020f755c3c082000",
properties: map[string]string{
"color": "aaabbb",
},
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `
{
"links": {
"self": "/api/v2/labels/020f755c3c082000"
},
"label": {
"id": "020f755c3c082000",
"name": "mylabel",
"properties": {
"color": "aaabbb"
}
}
}
`,
},
},
{
name: "label not found",
fields: fields{
&mock.LabelService{
UpdateLabelFn: func(ctx context.Context, id platform.ID, upd platform.LabelUpdate) (*platform.Label, error) {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "label not found",
}
},
},
},
args: args{
id: "020f755c3c082000",
properties: map[string]string{
"color": "aaabbb",
},
},
wants: wants{
statusCode: http.StatusNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewLabelHandler()
h.LabelService = tt.fields.LabelService
upd := platform.LabelUpdate{}
if len(tt.args.properties) > 0 {
upd.Properties = tt.args.properties
}
l, err := json.Marshal(upd)
if err != nil {
t.Fatalf("failed to marshal label update: %v", err)
}
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(l))
r = r.WithContext(context.WithValue(
context.Background(),
httprouter.ParamsKey,
httprouter.Params{
{
Key: "id",
Value: tt.args.id,
},
}))
w := httptest.NewRecorder()
h.handlePatchLabel(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. handlePatchLabel() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. handlePatchLabel() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. handlePatchLabel() = ***%v***", tt.name, diff)
}
})
}
}

View File

@ -41,7 +41,7 @@ const (
// TODO(desa): need a way to specify which secrets to delete. this should work for now
organizationsIDSecretsDeletePath = "/api/v2/orgs/:id/secrets/delete"
organizationsIDLabelsPath = "/api/v2/orgs/:id/labels"
organizationsIDLabelsNamePath = "/api/v2/orgs/:id/labels/:name"
organizationsIDLabelsIDPath = "/api/v2/orgs/:id/labels/:lid"
)
// NewOrgHandler returns a new instance of OrgHandler.
@ -78,8 +78,7 @@ func NewOrgHandler(mappingService platform.UserResourceMappingService,
h.HandlerFunc("GET", organizationsIDLabelsPath, newGetLabelsHandler(h.LabelService))
h.HandlerFunc("POST", organizationsIDLabelsPath, newPostLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", organizationsIDLabelsNamePath, newDeleteLabelHandler(h.LabelService))
h.HandlerFunc("PATCH", organizationsIDLabelsNamePath, newPatchLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", organizationsIDLabelsIDPath, newDeleteLabelHandler(h.LabelService))
return h
}

View File

@ -326,7 +326,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
$ref: "#/components/schemas/LabelMapping"
responses:
'200':
description: a list of all labels for a telegraf config
@ -345,7 +345,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/telegrafs/{telegrafID}/labels/{label}':
'/telegrafs/{telegrafID}/labels/{labelID}':
delete:
tags:
- Telegrafs
@ -359,11 +359,11 @@ paths:
required: true
description: ID of the telegraf config
- in: path
name: label
name: labelID
schema:
type: string
required: true
description: the label name
description: the label ID
responses:
'204':
description: delete has been accepted
@ -379,46 +379,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Telegrafs
summary: update a label from a telegraf config
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: telegrafID
schema:
type: string
required: true
description: ID of the telegraf config
- in: path
name: label
schema:
type: string
required: true
description: the label name
requestBody:
description: label update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
responses:
'200':
description: updated successfully
'404':
description: telegraf config not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/telegrafs/{telegrafID}/members':
get:
tags:
@ -1409,7 +1369,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
$ref: "#/components/schemas/LabelMapping"
responses:
'200':
description: a list of all labels for a view
@ -1428,7 +1388,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/views/{viewID}/labels/{label}':
'/views/{viewID}/labels/{labelID}':
delete:
tags:
- Views
@ -1442,11 +1402,11 @@ paths:
required: true
description: ID of the view
- in: path
name: label
name: labelID
schema:
type: string
required: true
description: the label name
description: the label id
responses:
'204':
description: delete has been accepted
@ -1462,46 +1422,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Views
summary: update a label from a view
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: viewID
schema:
type: string
required: true
description: ID of the view
- in: path
name: label
schema:
type: string
required: true
description: the label name
requestBody:
description: label update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
responses:
'200':
description: updated successfully
'404':
description: view not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/views/{viewID}/members':
get:
tags:
@ -1680,6 +1600,139 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
/labels:
post:
tags:
- Labels
summary: Create a label
requestBody:
description: label to create
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
responses:
'201':
description: Added label
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
get:
tags:
- Labels
summary: Get all labels
responses:
'200':
description: all labels
content:
application/json:
schema:
$ref: "#/components/schemas/Labels"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/labels/{labelID}:
get:
tags:
- Labels
summary: Get a label
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: labelID
schema:
type: string
required: true
description: ID of label to update
responses:
'200':
description: a label
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Labels
summary: Update a single label
requestBody:
description: label update
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LabelUpdate"
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: labelID
schema:
type: string
required: true
description: ID of label to update
responses:
'200':
description: Updated label
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
'404':
description: label not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
delete:
tags:
- Labels
summary: Delete a label
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: labelID
schema:
type: string
required: true
description: ID of label to delete
responses:
'204':
description: delete has been accepted
'404':
description: label not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/dashboards:
post:
tags:
@ -2148,7 +2201,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
$ref: "#/components/schemas/LabelMapping"
responses:
'200':
description: a list of all labels for a dashboard
@ -2167,6 +2220,40 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/dashboards/{dashboardID}/labels/{labelID}':
delete:
tags:
- Dashboards
summary: delete a label from a dashboard
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: dashboardID
schema:
type: string
required: true
description: ID of the dashboard
- in: path
name: labelID
schema:
type: string
required: true
description: the label id to delete
responses:
'204':
description: delete has been accepted
'404':
description: dashboard not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/dashboards/{dashboardID}/members':
get:
tags:
@ -2937,7 +3024,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
$ref: "#/components/schemas/LabelMapping"
responses:
'200':
description: a list of all labels for a bucket
@ -2956,7 +3043,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/buckets/{bucketID}/labels/{label}':
'/buckets/{bucketID}/labels/{labelID}':
delete:
tags:
- Buckets
@ -2970,11 +3057,11 @@ paths:
required: true
description: ID of the bucket
- in: path
name: label
name: labelID
schema:
type: string
required: true
description: the label name
description: the label id to delete
responses:
'204':
description: delete has been accepted
@ -2990,46 +3077,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Buckets
summary: update a label from a bucket
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: bucketID
schema:
type: string
required: true
description: ID of the bucket
- in: path
name: label
schema:
type: string
required: true
description: the label name
requestBody:
description: label update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
responses:
'200':
description: updated successfully
'404':
description: bucket not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/buckets/{bucketID}/members':
get:
tags:
@ -3386,7 +3433,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
$ref: "#/components/schemas/LabelMapping"
responses:
'200':
description: a list of all labels for an organization
@ -3405,7 +3452,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/orgs/{orgID}/labels/{label}':
'/orgs/{orgID}/labels/{labelID}':
delete:
tags:
- Organizations
@ -3419,11 +3466,11 @@ paths:
required: true
description: ID of the organization
- in: path
name: label
name: labelID
schema:
type: string
required: true
description: the label name
description: the label id
responses:
'204':
description: delete has been accepted
@ -3439,46 +3486,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Organizations
summary: update a label from an organization
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: orgID
schema:
type: string
required: true
description: ID of the organization
- in: path
name: label
schema:
type: string
required: true
description: the label name
requestBody:
description: label update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
responses:
'200':
description: updated successfully
'404':
description: organization not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/orgs/{orgID}/secrets':
get:
tags:
@ -4150,7 +4157,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
$ref: "#/components/schemas/LabelMapping"
responses:
'200':
description: a list of all labels for a task
@ -4171,7 +4178,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/tasks/{taskID}/labels/{label}':
'/tasks/{taskID}/labels/{labelID}':
delete:
tags:
- Tasks
@ -4185,11 +4192,11 @@ paths:
required: true
description: ID of the task
- in: path
name: label
name: labelID
schema:
type: string
required: true
description: the label name
description: the label id
responses:
'204':
description: delete has been accepted
@ -4205,46 +4212,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Tasks
summary: update a label from a task
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: taskID
schema:
type: string
required: true
description: ID of the task
- in: path
name: label
schema:
type: string
required: true
description: the label name
requestBody:
description: label update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
responses:
'200':
description: updated successfully
'404':
description: task not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/me:
get:
tags:
@ -7054,9 +7021,24 @@ components:
Label:
type: object
properties:
id:
readOnly: true
type: string
name:
type: string
properties:
type: object
description: Key/Value pairs associated with this label. Keys can be removed by sending an update with an empty value.
example: {"color": "ffb3b3", "description": "this is a description"}
LabelUpdate:
type: object
properties:
properties:
type: object
description: Key/Value pairs associated with this label. Keys can be removed by sending an update with an empty value.
example: {"color": "ffb3b3", "description": "this is a description"}
LabelMapping:
type: object
properties:
labelID:
type: string

View File

@ -47,7 +47,7 @@ const (
tasksIDRunsIDLogsPath = "/api/v2/tasks/:id/runs/:rid/logs"
tasksIDRunsIDRetryPath = "/api/v2/tasks/:id/runs/:rid/retry"
tasksIDLabelsPath = "/api/v2/tasks/:id/labels"
tasksIDLabelsNamePath = "/api/v2/tasks/:id/labels/:name"
tasksIDLabelsIDPath = "/api/v2/tasks/:id/labels/:lid"
)
// NewTaskHandler returns a new instance of TaskHandler.
@ -87,8 +87,7 @@ func NewTaskHandler(mappingService platform.UserResourceMappingService, labelSer
h.HandlerFunc("GET", tasksIDLabelsPath, newGetLabelsHandler(h.LabelService))
h.HandlerFunc("POST", tasksIDLabelsPath, newPostLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", tasksIDLabelsNamePath, newDeleteLabelHandler(h.LabelService))
h.HandlerFunc("PATCH", tasksIDLabelsNamePath, newPatchLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", tasksIDLabelsIDPath, newDeleteLabelHandler(h.LabelService))
return h
}
@ -157,7 +156,7 @@ func newTasksResponse(ctx context.Context, ts []*platform.Task, labelService pla
}
for i := range ts {
labels, _ := labelService.FindLabels(ctx, platform.LabelFilter{ResourceID: ts[i].ID})
labels, _ := labelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: ts[i].ID})
rs.Tasks[i] = newTaskResponse(*ts[i], labels)
}
return rs
@ -359,7 +358,7 @@ func (h *TaskHandler) handleGetTask(w http.ResponseWriter, r *http.Request) {
return
}
labels, err := h.LabelService.FindLabels(ctx, platform.LabelFilter{ResourceID: task.ID})
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: task.ID})
if err != nil {
EncodeError(ctx, err, w)
return
@ -408,7 +407,7 @@ func (h *TaskHandler) handleUpdateTask(w http.ResponseWriter, r *http.Request) {
return
}
labels, err := h.LabelService.FindLabels(ctx, platform.LabelFilter{ResourceID: task.ID})
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: task.ID})
if err != nil {
EncodeError(ctx, err, w)
return

View File

@ -15,6 +15,7 @@ import (
"github.com/influxdata/influxdb/logger"
"github.com/influxdata/influxdb/mock"
_ "github.com/influxdata/influxdb/query/builtin"
platformtesting "github.com/influxdata/influxdb/testing"
"github.com/julienschmidt/httprouter"
)
@ -76,11 +77,11 @@ func TestTaskHandler_handleGetTasks(t *testing.T) {
},
},
labelService: &mock.LabelService{
FindLabelsFn: func(ctx context.Context, f platform.LabelFilter) ([]*platform.Label, error) {
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
labels := []*platform.Label{
{
ResourceID: f.ResourceID,
Name: "label",
ID: platformtesting.MustIDBase16("fc3dc670a4be9b9a"),
Name: "label",
Properties: map[string]string{
"color": "fff000",
},
@ -112,7 +113,7 @@ func TestTaskHandler_handleGetTasks(t *testing.T) {
"name": "task1",
"labels": [
{
"resourceID": "0000000000000001",
"id": "fc3dc670a4be9b9a",
"name": "label",
"properties": {
"color": "fff000"
@ -141,7 +142,7 @@ func TestTaskHandler_handleGetTasks(t *testing.T) {
"name": "task2",
"labels": [
{
"resourceID": "0000000000000002",
"id": "fc3dc670a4be9b9a",
"name": "label",
"properties": {
"color": "fff000"

View File

@ -28,14 +28,14 @@ type TelegrafHandler struct {
}
const (
telegrafsPath = "/api/v2/telegrafs"
telegrafsIDPath = "/api/v2/telegrafs/:id"
telegrafsIDMembersPath = "/api/v2/telegrafs/:id/members"
telegrafsIDMembersIDPath = "/api/v2/telegrafs/:id/members/:userID"
telegrafsIDOwnersPath = "/api/v2/telegrafs/:id/owners"
telegrafsIDOwnersIDPath = "/api/v2/telegrafs/:id/owners/:userID"
telegrafsIDLabelsPath = "/api/v2/telegrafs/:id/labels"
telegrafsIDLabelsNamePath = "/api/v2/telegrafs/:id/labels/:name"
telegrafsPath = "/api/v2/telegrafs"
telegrafsIDPath = "/api/v2/telegrafs/:id"
telegrafsIDMembersPath = "/api/v2/telegrafs/:id/members"
telegrafsIDMembersIDPath = "/api/v2/telegrafs/:id/members/:userID"
telegrafsIDOwnersPath = "/api/v2/telegrafs/:id/owners"
telegrafsIDOwnersIDPath = "/api/v2/telegrafs/:id/owners/:userID"
telegrafsIDLabelsPath = "/api/v2/telegrafs/:id/labels"
telegrafsIDLabelsIDPath = "/api/v2/telegrafs/:id/labels/:lid"
)
// NewTelegrafHandler returns a new instance of TelegrafHandler.
@ -73,8 +73,7 @@ func NewTelegrafHandler(
h.HandlerFunc("GET", telegrafsIDLabelsPath, newGetLabelsHandler(h.LabelService))
h.HandlerFunc("POST", telegrafsIDLabelsPath, newPostLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", telegrafsIDLabelsNamePath, newDeleteLabelHandler(h.LabelService))
h.HandlerFunc("PATCH", telegrafsIDLabelsNamePath, newPatchLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", telegrafsIDLabelsIDPath, newDeleteLabelHandler(h.LabelService))
return h
}
@ -116,7 +115,7 @@ func newTelegrafResponses(ctx context.Context, tcs []*platform.TelegrafConfig, l
TelegrafConfigs: make([]telegrafResponse, len(tcs)),
}
for i, c := range tcs {
labels, _ := labelService.FindLabels(ctx, platform.LabelFilter{ResourceID: c.ID})
labels, _ := labelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: c.ID})
resp.TelegrafConfigs[i] = newTelegrafResponse(c, labels)
}
return resp
@ -177,7 +176,7 @@ func (h *TelegrafHandler) handleGetTelegraf(w http.ResponseWriter, r *http.Reque
w.WriteHeader(http.StatusOK)
w.Write([]byte(tc.TOML()))
case "application/json":
labels, err := h.LabelService.FindLabels(ctx, platform.LabelFilter{ResourceID: tc.ID})
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: tc.ID})
if err != nil {
EncodeError(ctx, err, w)
return
@ -312,7 +311,7 @@ func (h *TelegrafHandler) handlePutTelegraf(w http.ResponseWriter, r *http.Reque
return
}
labels, err := h.LabelService.FindLabels(ctx, platform.LabelFilter{ResourceID: tc.ID})
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: tc.ID})
if err != nil {
EncodeError(ctx, err, w)
return

View File

@ -27,14 +27,14 @@ type ViewHandler struct {
}
const (
viewsPath = "/api/v2/views"
viewsIDPath = "/api/v2/views/:id"
viewsIDMembersPath = "/api/v2/views/:id/members"
viewsIDMembersIDPath = "/api/v2/views/:id/members/:userID"
viewsIDOwnersPath = "/api/v2/views/:id/owners"
viewsIDOwnersIDPath = "/api/v2/views/:id/owners/:userID"
viewsIDLabelsPath = "/api/v2/views/:id/labels"
viewsIDLabelsNamePath = "/api/v2/views/:id/labels/:name"
viewsPath = "/api/v2/views"
viewsIDPath = "/api/v2/views/:id"
viewsIDMembersPath = "/api/v2/views/:id/members"
viewsIDMembersIDPath = "/api/v2/views/:id/members/:userID"
viewsIDOwnersPath = "/api/v2/views/:id/owners"
viewsIDOwnersIDPath = "/api/v2/views/:id/owners/:userID"
viewsIDLabelsPath = "/api/v2/views/:id/labels"
viewsIDLabelsIDPath = "/api/v2/views/:id/labels/:lid"
)
// NewViewHandler returns a new instance of ViewHandler.
@ -65,8 +65,7 @@ func NewViewHandler(mappingService platform.UserResourceMappingService, labelSer
h.HandlerFunc("GET", viewsIDLabelsPath, newGetLabelsHandler(h.LabelService))
h.HandlerFunc("POST", viewsIDLabelsPath, newPostLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", viewsIDLabelsNamePath, newDeleteLabelHandler(h.LabelService))
h.HandlerFunc("PATCH", viewsIDLabelsNamePath, newPatchLabelHandler(h.LabelService))
h.HandlerFunc("DELETE", viewsIDLabelsIDPath, newDeleteLabelHandler(h.LabelService))
return h
}

View File

@ -308,7 +308,9 @@ func (s *Service) DeleteBucket(ctx context.Context, id platform.ID) error {
}
}
s.bucketKV.Delete(id.String())
return s.deleteLabel(ctx, platform.LabelFilter{ResourceID: id})
// return s.deleteLabel(ctx, platform.LabelFilter{ResourceID: id})
return nil
}
// DeleteOrganizationBuckets removes all the buckets for a given org

View File

@ -209,13 +209,13 @@ func (s *Service) DeleteDashboard(ctx context.Context, id platform.ID) error {
}
}
s.dashboardKV.Delete(id.String())
err := s.deleteLabel(ctx, platform.LabelFilter{ResourceID: id})
if err != nil {
return &platform.Error{
Err: err,
Op: op,
}
}
// err := s.deleteLabel(ctx, platform.LabelFilter{ResourceID: id})
// if err != nil {
// return &platform.Error{
// Err: err,
// Op: op,
// }
// }
return nil
}

View File

@ -8,14 +8,13 @@ import (
platform "github.com/influxdata/influxdb"
)
func encodeLabelKey(resourceID platform.ID, name string) string {
return path.Join(resourceID.String(), name)
}
func (s *Service) loadLabel(ctx context.Context, resourceID platform.ID, name string) (*platform.Label, error) {
i, ok := s.labelKV.Load(encodeLabelKey(resourceID, name))
func (s *Service) loadLabel(ctx context.Context, id platform.ID) (*platform.Label, error) {
i, ok := s.labelKV.Load(id.String())
if !ok {
return nil, platform.ErrLabelNotFound
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "label not found",
}
}
l, ok := i.(platform.Label)
@ -26,10 +25,6 @@ func (s *Service) loadLabel(ctx context.Context, resourceID platform.ID, name st
return &l, nil
}
func (s *Service) FindLabelBy(ctx context.Context, resourceID platform.ID, name string) (*platform.Label, error) {
return s.loadLabel(ctx, resourceID, name)
}
func (s *Service) forEachLabel(ctx context.Context, fn func(m *platform.Label) bool) error {
var err error
s.labelKV.Range(func(k, v interface{}) bool {
@ -44,6 +39,20 @@ func (s *Service) forEachLabel(ctx context.Context, fn func(m *platform.Label) b
return err
}
func (s *Service) forEachLabelMapping(ctx context.Context, fn func(m *platform.LabelMapping) bool) error {
var err error
s.labelMappingKV.Range(func(k, v interface{}) bool {
m, ok := v.(platform.LabelMapping)
if !ok {
err = fmt.Errorf("type %T is not a label mapping", v)
return false
}
return fn(&m)
})
return err
}
func (s *Service) filterLabels(ctx context.Context, fn func(m *platform.Label) bool) ([]*platform.Label, error) {
labels := []*platform.Label{}
err := s.forEachLabel(ctx, func(l *platform.Label) bool {
@ -60,9 +69,35 @@ func (s *Service) filterLabels(ctx context.Context, fn func(m *platform.Label) b
return labels, nil
}
func (s *Service) filterLabelMappings(ctx context.Context, fn func(m *platform.LabelMapping) bool) ([]*platform.LabelMapping, error) {
mappings := []*platform.LabelMapping{}
err := s.forEachLabelMapping(ctx, func(m *platform.LabelMapping) bool {
if fn(m) {
mappings = append(mappings, m)
}
return true
})
if err != nil {
return nil, err
}
return mappings, nil
}
func encodeLabelMappingKey(m *platform.LabelMapping) string {
return path.Join(m.ResourceID.String(), m.LabelID.String())
}
// FindLabelByID returns a single user by ID.
func (s *Service) FindLabelByID(ctx context.Context, id platform.ID) (*platform.Label, error) {
return s.loadLabel(ctx, id)
}
// FindLabels will retrieve a list of labels from storage.
func (s *Service) FindLabels(ctx context.Context, filter platform.LabelFilter, opt ...platform.FindOptions) ([]*platform.Label, error) {
if filter.ResourceID.Valid() && filter.Name != "" {
l, err := s.FindLabelBy(ctx, filter.ResourceID, filter.Name)
if filter.ID.Valid() {
l, err := s.FindLabelByID(ctx, filter.ID)
if err != nil {
return nil, err
}
@ -70,8 +105,7 @@ func (s *Service) FindLabels(ctx context.Context, filter platform.LabelFilter, o
}
filterFunc := func(label *platform.Label) bool {
return (!filter.ResourceID.Valid() || (filter.ResourceID == label.ResourceID)) &&
(filter.Name == "" || (filter.Name == label.Name))
return (filter.Name == "" || (filter.Name == label.Name))
}
labels, err := s.filterLabels(ctx, filterFunc)
@ -82,27 +116,60 @@ func (s *Service) FindLabels(ctx context.Context, filter platform.LabelFilter, o
return labels, nil
}
func (s *Service) CreateLabel(ctx context.Context, l *platform.Label) error {
label, _ := s.FindLabelBy(ctx, l.ResourceID, l.Name)
if label != nil {
return &platform.Error{
Code: platform.EConflict,
Op: OpPrefix + platform.OpCreateLabel,
Msg: fmt.Sprintf("label %s already exists", l.Name),
}
// FindResourceLabels returns a list of labels that are mapped to a resource.
func (s *Service) FindResourceLabels(ctx context.Context, filter platform.LabelMappingFilter) ([]*platform.Label, error) {
filterFunc := func(mapping *platform.LabelMapping) bool {
return (filter.ResourceID.String() == mapping.ResourceID.String())
}
s.labelKV.Store(encodeLabelKey(l.ResourceID, l.Name), *l)
mappings, err := s.filterLabelMappings(ctx, filterFunc)
if err != nil {
return nil, err
}
ls := []*platform.Label{}
for _, m := range mappings {
l, err := s.FindLabelByID(ctx, *m.LabelID)
if err != nil {
return nil, err
}
ls = append(ls, l)
}
return ls, nil
}
// CreateLabel creates a new label.
func (s *Service) CreateLabel(ctx context.Context, l *platform.Label) error {
l.ID = s.IDGenerator.ID()
s.labelKV.Store(l.ID, *l)
return nil
}
func (s *Service) UpdateLabel(ctx context.Context, l *platform.Label, upd platform.LabelUpdate) (*platform.Label, error) {
label, err := s.FindLabelBy(ctx, l.ResourceID, l.Name)
// CreateLabelMapping creates a mapping that associates a label to a resource.
func (s *Service) CreateLabelMapping(ctx context.Context, m *platform.LabelMapping) error {
_, err := s.FindLabelByID(ctx, *m.LabelID)
if err != nil {
return &platform.Error{
Err: err,
Op: platform.OpCreateLabel,
}
}
s.labelMappingKV.Store(encodeLabelMappingKey(m), *m)
return nil
}
// UpdateLabel updates a label.
func (s *Service) UpdateLabel(ctx context.Context, id platform.ID, upd platform.LabelUpdate) (*platform.Label, error) {
label, err := s.FindLabelByID(ctx, id)
if err != nil {
return nil, &platform.Error{
Code: platform.ENotFound,
Op: OpPrefix + platform.OpUpdateLabel,
Err: err,
Msg: "label not found",
}
}
@ -126,38 +193,36 @@ func (s *Service) UpdateLabel(ctx context.Context, l *platform.Label, upd platfo
}
}
s.labelKV.Store(encodeLabelKey(label.ResourceID, label.Name), *label)
s.labelKV.Store(label.ID.String(), *label)
return label, nil
}
// PutLabel writes a label directly to the database without generating IDs
// or making checks.
func (s *Service) PutLabel(ctx context.Context, l *platform.Label) error {
s.labelKV.Store(encodeLabelKey(l.ResourceID, l.Name), *l)
s.labelKV.Store(l.ID.String(), *l)
return nil
}
func (s *Service) DeleteLabel(ctx context.Context, l platform.Label) error {
label, err := s.FindLabelBy(ctx, l.ResourceID, l.Name)
// DeleteLabel deletes a label.
func (s *Service) DeleteLabel(ctx context.Context, id platform.ID) error {
label, err := s.FindLabelByID(ctx, id)
if label == nil && err != nil {
return &platform.Error{
Code: platform.ENotFound,
Op: OpPrefix + platform.OpDeleteLabel,
Err: platform.ErrLabelNotFound,
Msg: "label not found",
}
}
s.labelKV.Delete(encodeLabelKey(l.ResourceID, l.Name))
s.labelKV.Delete(id.String())
return nil
}
func (s *Service) deleteLabel(ctx context.Context, filter platform.LabelFilter) error {
labels, err := s.FindLabels(ctx, filter)
if labels == nil && err != nil {
return err
}
for _, l := range labels {
s.labelKV.Delete(encodeLabelKey(l.ResourceID, l.Name))
}
// DeleteLabelMapping deletes a label mapping.
func (s *Service) DeleteLabelMapping(ctx context.Context, m *platform.LabelMapping) error {
s.labelMappingKV.Delete(encodeLabelMappingKey(m))
return nil
}

View File

@ -10,16 +10,24 @@ import (
func initLabelService(f platformtesting.LabelFields, t *testing.T) (platform.LabelService, string, func()) {
s := NewService()
ctx := context.TODO()
for _, m := range f.Labels {
if err := s.CreateLabel(ctx, m); err != nil {
s.IDGenerator = f.IDGenerator
ctx := context.Background()
for _, l := range f.Labels {
if err := s.PutLabel(ctx, l); err != nil {
t.Fatalf("failed to populate labels")
}
}
for _, m := range f.Mappings {
if err := s.CreateLabelMapping(ctx, m); err != nil {
t.Fatalf("failed to populate label mappings")
}
}
return s, OpPrefix, func() {}
}
func TestLabelService(t *testing.T) {
t.Parallel()
platformtesting.LabelService(initLabelService, t)
}

View File

@ -24,6 +24,7 @@ type Service struct {
dbrpMappingKV sync.Map
userResourceMappingKV sync.Map
labelKV sync.Map
labelMappingKV sync.Map
scraperTargetKV sync.Map
telegrafConfigKV sync.Map
onboardingKV sync.Map

View File

@ -8,41 +8,52 @@ import (
const ErrLabelNotFound = ChronografError("label not found")
const (
OpFindLabels = "FindLabels"
OpCreateLabel = "CreateLabel"
OpUpdateLabel = "UpdateLabel"
OpDeleteLabel = "DeleteLabel"
OpFindLabels = "FindLabels"
OpFindLabelByID = "FindLabelByID"
OpFindLabelMapping = "FindLabelMapping"
OpCreateLabel = "CreateLabel"
OpCreateLabelMapping = "CreateLabelMapping"
OpUpdateLabel = "UpdateLabel"
OpDeleteLabel = "DeleteLabel"
OpDeleteLabelMapping = "DeleteLabelMapping"
)
// LabelService represents a service for managing resource labels
type LabelService interface {
// FindLabelByID a single label by ID.
FindLabelByID(ctx context.Context, id ID) (*Label, error)
// FindLabels returns a list of labels that match a filter
FindLabels(ctx context.Context, filter LabelFilter, opt ...FindOptions) ([]*Label, error)
// FindResourceLabels returns a list of labels that belong to a resource
FindResourceLabels(ctx context.Context, filter LabelMappingFilter) ([]*Label, error)
// CreateLabel creates a new label
CreateLabel(ctx context.Context, l *Label) error
// CreateLabel maps a resource to an existing label
CreateLabelMapping(ctx context.Context, m *LabelMapping) error
// UpdateLabel updates a label with a changeset.
UpdateLabel(ctx context.Context, l *Label, upd LabelUpdate) (*Label, error)
UpdateLabel(ctx context.Context, id ID, upd LabelUpdate) (*Label, error)
// DeleteLabel deletes a label
DeleteLabel(ctx context.Context, l Label) error
DeleteLabel(ctx context.Context, id ID) error
// DeleteLabelMapping deletes a label mapping
DeleteLabelMapping(ctx context.Context, m *LabelMapping) error
}
// Label is a tag set on a resource, typically used for filtering on a UI.
type Label struct {
ResourceID ID `json:"resourceID"`
ID ID `json:"id,omitempty"`
Name string `json:"name"`
Properties map[string]string `json:"properties"`
Properties map[string]string `json:"properties,omitempty"`
}
// Validate returns an error if the label is invalid.
func (l *Label) Validate() error {
if !l.ResourceID.Valid() {
return &Error{
Code: EInvalid,
Msg: "resourceID is required",
}
}
if l.Name == "" {
return &Error{
Code: EInvalid,
@ -53,13 +64,38 @@ func (l *Label) Validate() error {
return nil
}
// LabelMapping is used to map resource to its labels.
// It should not be shared directly over the HTTP API.
type LabelMapping struct {
LabelID *ID `json:"labelID"`
ResourceID *ID
}
// Validate returns an error if the mapping is invalid.
func (l *LabelMapping) Validate() error {
if !l.ResourceID.Valid() {
return &Error{
Code: EInvalid,
Msg: "resourceID is required",
}
}
return nil
}
// LabelUpdate represents a changeset for a label.
// Only fields which are set are updated.
type LabelUpdate struct {
Properties map[string]string `json:"properties,omitempty"`
}
// LabelFilter represents a set of filters that restrict the returned results.
type LabelFilter struct {
ResourceID ID
Name string
ID ID
Name string
}
// LabelMappingFilter represents a set of filters that restrict the returned results.
type LabelMappingFilter struct {
ResourceID ID
}

View File

@ -4,7 +4,6 @@ import (
"testing"
platform "github.com/influxdata/influxdb"
platformtesting "github.com/influxdata/influxdb/testing"
)
func TestLabelValidate(t *testing.T) {
@ -19,31 +18,20 @@ func TestLabelValidate(t *testing.T) {
}{
{
name: "valid label",
fields: fields{
ResourceID: platformtesting.MustIDBase16("020f755c3c082000"),
Name: "iot",
},
},
{
name: "label requires a resourceid",
fields: fields{
Name: "iot",
},
wantErr: true,
},
{
name: "label requires a name",
fields: fields{
ResourceID: platformtesting.MustIDBase16("020f755c3c082000"),
},
name: "label requires a name",
fields: fields{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := platform.Label{
ResourceID: tt.fields.ResourceID,
Name: tt.fields.Name,
Name: tt.fields.Name,
}
if err := m.Validate(); (err != nil) != tt.wantErr {
t.Errorf("Label.Validate() error = %v, wantErr %v", err, tt.wantErr)

View File

@ -10,41 +10,73 @@ var _ platform.LabelService = &LabelService{}
// LabelService is a mock implementation of platform.LabelService
type LabelService struct {
FindLabelsFn func(context.Context, platform.LabelFilter) ([]*platform.Label, error)
CreateLabelFn func(context.Context, *platform.Label) error
UpdateLabelFn func(context.Context, *platform.Label, platform.LabelUpdate) (*platform.Label, error)
DeleteLabelFn func(context.Context, platform.Label) error
FindLabelByIDFn func(ctx context.Context, id platform.ID) (*platform.Label, error)
FindLabelsFn func(context.Context, platform.LabelFilter) ([]*platform.Label, error)
FindResourceLabelsFn func(context.Context, platform.LabelMappingFilter) ([]*platform.Label, error)
CreateLabelFn func(context.Context, *platform.Label) error
CreateLabelMappingFn func(context.Context, *platform.LabelMapping) error
UpdateLabelFn func(context.Context, platform.ID, platform.LabelUpdate) (*platform.Label, error)
DeleteLabelFn func(context.Context, platform.ID) error
DeleteLabelMappingFn func(context.Context, *platform.LabelMapping) error
}
// NewLabelService returns a mock of LabelService
// where its methods will return zero values.
func NewLabelService() *LabelService {
return &LabelService{
FindLabelByIDFn: func(ctx context.Context, id platform.ID) (*platform.Label, error) {
return nil, nil
},
FindLabelsFn: func(context.Context, platform.LabelFilter) ([]*platform.Label, error) {
return nil, nil
},
CreateLabelFn: func(context.Context, *platform.Label) error { return nil },
UpdateLabelFn: func(context.Context, *platform.Label, platform.LabelUpdate) (*platform.Label, error) { return nil, nil },
DeleteLabelFn: func(context.Context, platform.Label) error { return nil },
FindResourceLabelsFn: func(context.Context, platform.LabelMappingFilter) ([]*platform.Label, error) {
return []*platform.Label{}, nil
},
CreateLabelFn: func(context.Context, *platform.Label) error { return nil },
CreateLabelMappingFn: func(context.Context, *platform.LabelMapping) error { return nil },
UpdateLabelFn: func(context.Context, platform.ID, platform.LabelUpdate) (*platform.Label, error) { return nil, nil },
DeleteLabelFn: func(context.Context, platform.ID) error { return nil },
DeleteLabelMappingFn: func(context.Context, *platform.LabelMapping) error { return nil },
}
}
// FindLabelByID finds mappings by their ID
func (s *LabelService) FindLabelByID(ctx context.Context, id platform.ID) (*platform.Label, error) {
return s.FindLabelByIDFn(ctx, id)
}
// FindLabels finds mappings that match a given filter.
func (s *LabelService) FindLabels(ctx context.Context, filter platform.LabelFilter, opt ...platform.FindOptions) ([]*platform.Label, error) {
return s.FindLabelsFn(ctx, filter)
}
// FindResourceLabels finds mappings that match a given filter.
func (s *LabelService) FindResourceLabels(ctx context.Context, filter platform.LabelMappingFilter) ([]*platform.Label, error) {
return s.FindResourceLabelsFn(ctx, filter)
}
// CreateLabel creates a new Label.
func (s *LabelService) CreateLabel(ctx context.Context, l *platform.Label) error {
return s.CreateLabelFn(ctx, l)
}
// CreateLabelMapping creates a new Label mapping.
func (s *LabelService) CreateLabelMapping(ctx context.Context, m *platform.LabelMapping) error {
return s.CreateLabelMappingFn(ctx, m)
}
// UpdateLabel updates a label.
func (s *LabelService) UpdateLabel(ctx context.Context, l *platform.Label, upd platform.LabelUpdate) (*platform.Label, error) {
return s.UpdateLabelFn(ctx, l, upd)
func (s *LabelService) UpdateLabel(ctx context.Context, id platform.ID, upd platform.LabelUpdate) (*platform.Label, error) {
return s.UpdateLabelFn(ctx, id, upd)
}
// DeleteLabel removes a Label.
func (s *LabelService) DeleteLabel(ctx context.Context, l platform.Label) error {
return s.DeleteLabelFn(ctx, l)
func (s *LabelService) DeleteLabel(ctx context.Context, id platform.ID) error {
return s.DeleteLabelFn(ctx, id)
}
// DeleteLabelMapping removes a Label mapping.
func (s *LabelService) DeleteLabelMapping(ctx context.Context, m *platform.LabelMapping) error {
return s.DeleteLabelMappingFn(ctx, m)
}

View File

@ -8,6 +8,12 @@ import (
"github.com/google/go-cmp/cmp"
platform "github.com/influxdata/influxdb"
"github.com/influxdata/influxdb/mock"
)
const (
labelOneID = "41a9f7288d4e2d64"
labelTwoID = "b7c5355e1134b11c"
)
var labelCmpOptions = cmp.Options{
@ -17,17 +23,17 @@ var labelCmpOptions = cmp.Options{
cmp.Transformer("Sort", func(in []*platform.Label) []*platform.Label {
out := append([]*platform.Label(nil), in...) // Copy input to avoid mutating it
sort.Slice(out, func(i, j int) bool {
if out[i].Name != out[j].Name {
return out[i].Name < out[j].Name
}
return out[i].ResourceID.String() < out[j].ResourceID.String()
return out[i].Name < out[j].Name
})
return out
}),
}
// LabelFields include the IDGenerator, labels and their mappings
type LabelFields struct {
Labels []*platform.Label
Labels []*platform.Label
Mappings []*platform.LabelMapping
IDGenerator platform.IDGenerator
}
type labelServiceF func(
@ -48,10 +54,18 @@ func LabelService(
name: "CreateLabel",
fn: CreateLabel,
},
{
name: "CreateLabelMapping",
fn: CreateLabelMapping,
},
{
name: "FindLabels",
fn: FindLabels,
},
{
name: "FindLabelByID",
fn: FindLabelByID,
},
{
name: "UpdateLabel",
fn: UpdateLabel,
@ -60,6 +74,10 @@ func LabelService(
name: "DeleteLabel",
fn: DeleteLabel,
},
{
name: "DeleteLabelMapping",
fn: DeleteLabelMapping,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -89,17 +107,12 @@ func CreateLabel(
{
name: "basic create label",
fields: LabelFields{
Labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
},
IDGenerator: mock.NewIDGenerator(labelOneID, t),
Labels: []*platform.Label{},
},
args: args{
label: &platform.Label{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag2",
Name: "Tag2",
Properties: map[string]string{
"color": "fff000",
},
@ -108,12 +121,8 @@ func CreateLabel(
wants: wants{
labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag2",
ID: MustIDBase16(labelOneID),
Name: "Tag2",
Properties: map[string]string{
"color": "fff000",
},
@ -121,36 +130,34 @@ func CreateLabel(
},
},
},
{
name: "duplicate labels fail",
fields: LabelFields{
Labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
},
},
args: args{
label: &platform.Label{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
},
wants: wants{
labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
},
err: &platform.Error{
Code: platform.EConflict,
Op: platform.OpCreateLabel,
Msg: "label Tag1 already exists",
},
},
},
// {
// name: "duplicate labels fail",
// fields: LabelFields{
// IDGenerator: mock.NewIDGenerator(labelTwoID, t),
// Labels: []*platform.Label{
// {
// Name: "Tag1",
// },
// },
// },
// args: args{
// label: &platform.Label{
// Name: "Tag1",
// },
// },
// wants: wants{
// labels: []*platform.Label{
// {
// Name: "Tag1",
// },
// },
// err: &platform.Error{
// Code: platform.EConflict,
// Op: platform.OpCreateLabel,
// Msg: "label Tag1 already exists",
// },
// },
// },
}
for _, tt := range tests {
@ -161,7 +168,7 @@ func CreateLabel(
err := s.CreateLabel(ctx, tt.args.label)
diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)
defer s.DeleteLabel(ctx, *tt.args.label)
defer s.DeleteLabel(ctx, tt.args.label.ID)
labels, err := s.FindLabels(ctx, platform.LabelFilter{})
if err != nil {
@ -197,12 +204,12 @@ func FindLabels(
fields: LabelFields{
Labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag2",
ID: MustIDBase16(labelTwoID),
Name: "Tag2",
},
},
},
@ -212,12 +219,12 @@ func FindLabels(
wants: wants{
labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag2",
ID: MustIDBase16(labelTwoID),
Name: "Tag2",
},
},
},
@ -227,30 +234,25 @@ func FindLabels(
fields: LabelFields{
Labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag2",
},
{
ResourceID: MustIDBase16(bucketTwoID),
Name: "Tag1",
ID: MustIDBase16(labelTwoID),
Name: "Tag2",
},
},
},
args: args{
filter: platform.LabelFilter{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
Name: "Tag1",
},
},
wants: wants{
labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
},
},
@ -272,13 +274,88 @@ func FindLabels(
}
}
func FindLabelByID(
init func(LabelFields, *testing.T) (platform.LabelService, string, func()),
t *testing.T,
) {
type args struct {
id platform.ID
}
type wants struct {
err error
label *platform.Label
}
tests := []struct {
name string
fields LabelFields
args args
wants wants
}{
{
name: "find label by ID",
fields: LabelFields{
Labels: []*platform.Label{
{
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
{
ID: MustIDBase16(labelTwoID),
Name: "Tag2",
},
},
},
args: args{
id: MustIDBase16(labelOneID),
},
wants: wants{
label: &platform.Label{
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
},
},
{
name: "label does not exist",
fields: LabelFields{
Labels: []*platform.Label{},
},
args: args{
id: MustIDBase16(labelOneID),
},
wants: wants{
err: &platform.Error{
Code: platform.ENotFound,
Op: platform.OpFindLabelByID,
Msg: "label not found",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, opPrefix, done := init(tt.fields, t)
defer done()
ctx := context.Background()
label, err := s.FindLabelByID(ctx, tt.args.id)
diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)
if diff := cmp.Diff(label, tt.wants.label, labelCmpOptions...); diff != "" {
t.Errorf("labels are different -got/+want\ndiff %s", diff)
}
})
}
}
func UpdateLabel(
init func(LabelFields, *testing.T) (platform.LabelService, string, func()),
t *testing.T,
) {
type args struct {
label platform.Label
update platform.LabelUpdate
labelID platform.ID
update platform.LabelUpdate
}
type wants struct {
err error
@ -296,16 +373,13 @@ func UpdateLabel(
fields: LabelFields{
Labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
},
},
args: args{
label: platform.Label{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
labelID: MustIDBase16(labelOneID),
update: platform.LabelUpdate{
Properties: map[string]string{
"color": "fff000",
@ -315,8 +389,8 @@ func UpdateLabel(
wants: wants{
labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
Properties: map[string]string{
"color": "fff000",
},
@ -329,8 +403,8 @@ func UpdateLabel(
fields: LabelFields{
Labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
Properties: map[string]string{
"color": "fff000",
"description": "description",
@ -339,10 +413,7 @@ func UpdateLabel(
},
},
args: args{
label: platform.Label{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
labelID: MustIDBase16(labelOneID),
update: platform.LabelUpdate{
Properties: map[string]string{
"color": "abc123",
@ -352,8 +423,8 @@ func UpdateLabel(
wants: wants{
labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
Properties: map[string]string{
"color": "abc123",
"description": "description",
@ -367,8 +438,8 @@ func UpdateLabel(
fields: LabelFields{
Labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
Properties: map[string]string{
"color": "fff000",
"description": "description",
@ -377,10 +448,7 @@ func UpdateLabel(
},
},
args: args{
label: platform.Label{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
labelID: MustIDBase16(labelOneID),
update: platform.LabelUpdate{
Properties: map[string]string{
"description": "",
@ -390,8 +458,8 @@ func UpdateLabel(
wants: wants{
labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
Properties: map[string]string{
"color": "fff000",
},
@ -443,10 +511,7 @@ func UpdateLabel(
Labels: []*platform.Label{},
},
args: args{
label: platform.Label{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
labelID: MustIDBase16(labelOneID),
update: platform.LabelUpdate{
Properties: map[string]string{
"color": "fff000",
@ -458,6 +523,7 @@ func UpdateLabel(
err: &platform.Error{
Code: platform.ENotFound,
Op: platform.OpUpdateLabel,
Msg: "label not found",
},
},
},
@ -468,7 +534,7 @@ func UpdateLabel(
s, opPrefix, done := init(tt.fields, t)
defer done()
ctx := context.Background()
_, err := s.UpdateLabel(ctx, &tt.args.label, tt.args.update)
_, err := s.UpdateLabel(ctx, tt.args.labelID, tt.args.update)
diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)
labels, err := s.FindLabels(ctx, platform.LabelFilter{})
@ -487,7 +553,7 @@ func DeleteLabel(
t *testing.T,
) {
type args struct {
label platform.Label
labelID platform.ID
}
type wants struct {
err error
@ -505,26 +571,23 @@ func DeleteLabel(
fields: LabelFields{
Labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag2",
ID: MustIDBase16(labelTwoID),
Name: "Tag2",
},
},
},
args: args{
label: platform.Label{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
},
labelID: MustIDBase16(labelOneID),
},
wants: wants{
labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag2",
ID: MustIDBase16(labelTwoID),
Name: "Tag2",
},
},
},
@ -534,27 +597,25 @@ func DeleteLabel(
fields: LabelFields{
Labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
},
},
args: args{
label: platform.Label{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag2",
},
labelID: MustIDBase16(labelTwoID),
},
wants: wants{
labels: []*platform.Label{
{
ResourceID: MustIDBase16(bucketOneID),
Name: "Tag1",
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
},
err: &platform.Error{
Code: platform.ENotFound,
Op: platform.OpDeleteLabel,
Msg: "label not found",
},
},
},
@ -565,7 +626,7 @@ func DeleteLabel(
s, opPrefix, done := init(tt.fields, t)
defer done()
ctx := context.Background()
err := s.DeleteLabel(ctx, tt.args.label)
err := s.DeleteLabel(ctx, tt.args.labelID)
diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)
labels, err := s.FindLabels(ctx, platform.LabelFilter{})
@ -578,3 +639,192 @@ func DeleteLabel(
})
}
}
func CreateLabelMapping(
init func(LabelFields, *testing.T) (platform.LabelService, string, func()),
t *testing.T,
) {
type args struct {
mapping *platform.LabelMapping
filter *platform.LabelMappingFilter
}
type wants struct {
err error
labels []*platform.Label
}
tests := []struct {
name string
fields LabelFields
args args
wants wants
}{
{
name: "create label mapping",
fields: LabelFields{
Labels: []*platform.Label{
{
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
},
},
args: args{
mapping: &platform.LabelMapping{
LabelID: IDPtr(MustIDBase16(labelOneID)),
ResourceID: IDPtr(MustIDBase16(bucketOneID)),
},
filter: &platform.LabelMappingFilter{
ResourceID: MustIDBase16(bucketOneID),
},
},
wants: wants{
labels: []*platform.Label{
{
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
},
},
},
{
name: "mapping to a nonexistent label",
fields: LabelFields{
IDGenerator: mock.NewIDGenerator(labelOneID, t),
Labels: []*platform.Label{},
},
args: args{
mapping: &platform.LabelMapping{
LabelID: IDPtr(MustIDBase16(labelOneID)),
ResourceID: IDPtr(MustIDBase16(bucketOneID)),
},
},
wants: wants{
err: &platform.Error{
Code: platform.ENotFound,
Op: platform.OpDeleteLabel,
Msg: "label not found",
},
},
},
// {
// name: "duplicate label mappings",
// fields: LabelFields{
// IDGenerator: mock.NewIDGenerator(labelOneID, t),
// Labels: []*platform.Label{},
// },
// args: args{
// label: &platform.Label{
// Name: "Tag2",
// Properties: map[string]string{
// "color": "fff000",
// },
// },
// },
// wants: wants{
// labels: []*platform.Label{
// {
// ID: MustIDBase16(labelOneID),
// Name: "Tag2",
// Properties: map[string]string{
// "color": "fff000",
// },
// },
// },
// },
// },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, opPrefix, done := init(tt.fields, t)
defer done()
ctx := context.Background()
err := s.CreateLabelMapping(ctx, tt.args.mapping)
diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)
defer s.DeleteLabelMapping(ctx, tt.args.mapping)
if tt.args.filter == nil {
return
}
labels, err := s.FindResourceLabels(ctx, *tt.args.filter)
if err != nil {
t.Fatalf("failed to retrieve labels: %v", err)
}
if diff := cmp.Diff(labels, tt.wants.labels, labelCmpOptions...); diff != "" {
t.Errorf("labels are different -got/+want\ndiff %s", diff)
}
})
}
}
func DeleteLabelMapping(
init func(LabelFields, *testing.T) (platform.LabelService, string, func()),
t *testing.T,
) {
type args struct {
mapping *platform.LabelMapping
filter platform.LabelMappingFilter
}
type wants struct {
err error
labels []*platform.Label
}
tests := []struct {
name string
fields LabelFields
args args
wants wants
}{
{
name: "delete label mapping",
fields: LabelFields{
Labels: []*platform.Label{
{
ID: MustIDBase16(labelOneID),
Name: "Tag1",
},
},
Mappings: []*platform.LabelMapping{
{
LabelID: IDPtr(MustIDBase16(labelOneID)),
ResourceID: IDPtr(MustIDBase16(bucketOneID)),
},
},
},
args: args{
mapping: &platform.LabelMapping{
LabelID: IDPtr(MustIDBase16(labelOneID)),
ResourceID: IDPtr(MustIDBase16(bucketOneID)),
},
filter: platform.LabelMappingFilter{
ResourceID: MustIDBase16(bucketOneID),
},
},
wants: wants{
labels: []*platform.Label{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, opPrefix, done := init(tt.fields, t)
defer done()
ctx := context.Background()
err := s.DeleteLabelMapping(ctx, tt.args.mapping)
diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t)
labels, err := s.FindResourceLabels(ctx, tt.args.filter)
if err != nil {
t.Fatalf("failed to retrieve labels: %v", err)
}
if diff := cmp.Diff(labels, tt.wants.labels, labelCmpOptions...); diff != "" {
t.Errorf("labels are different -got/+want\ndiff %s", diff)
}
})
}
}