diff --git a/bolt/bucket.go b/bolt/bucket.go index 75f8f48000..1b2663825f 100644 --- a/bolt/bucket.go +++ b/bolt/bucket.go @@ -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 } diff --git a/bolt/dashboard.go b/bolt/dashboard.go index 5504cbc8d4..21163a961a 100644 --- a/bolt/dashboard.go +++ b/bolt/dashboard.go @@ -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, diff --git a/bolt/label.go b/bolt/label.go index e518ad82fc..121e6b1574 100644 --- a/bolt/label.go +++ b/bolt/label.go @@ -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 +// } diff --git a/bolt/label_test.go b/bolt/label_test.go index bd4574bdc1..4bcace1f2d 100644 --- a/bolt/label_test.go +++ b/bolt/label_test.go @@ -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) } } diff --git a/http/api_handler.go b/http/api_handler.go index 130d647fb3..a41a7fbfe9 100644 --- a/http/api_handler.go +++ b/http/api_handler.go @@ -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 diff --git a/http/bucket_service.go b/http/bucket_service.go index e414750d4b..adc5ea2716 100644 --- a/http/bucket_service.go +++ b/http/bucket_service.go @@ -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 diff --git a/http/bucket_test.go b/http/bucket_test.go index 2fefa4c940..15c9a78706 100644 --- a/http/bucket_test.go +++ b/http/bucket_test.go @@ -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" diff --git a/http/dashboard_service.go b/http/dashboard_service.go index e4672b6777..0f2510f1da 100644 --- a/http/dashboard_service.go +++ b/http/dashboard_service.go @@ -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 diff --git a/http/dashboard_test.go b/http/dashboard_test.go index 1da6272aed..f318e38fd1 100644 --- a/http/dashboard_test.go +++ b/http/dashboard_test.go @@ -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", diff --git a/http/label_service.go b/http/label_service.go index 6b966adec0..f55e33563b 100644 --- a/http/label_service.go +++ b/http/label_service.go @@ -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()) } diff --git a/http/label_test.go b/http/label_test.go new file mode 100644 index 0000000000..cb9edcf4ff --- /dev/null +++ b/http/label_test.go @@ -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) + } + }) + } +} diff --git a/http/org_service.go b/http/org_service.go index bb5b0b96f8..e17c431936 100644 --- a/http/org_service.go +++ b/http/org_service.go @@ -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 } diff --git a/http/swagger.yml b/http/swagger.yml index 2538b7d1c1..a5ba9c0e5b 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -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 diff --git a/http/task_service.go b/http/task_service.go index 45b15df9e6..94457aba21 100644 --- a/http/task_service.go +++ b/http/task_service.go @@ -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 diff --git a/http/task_service_test.go b/http/task_service_test.go index 486dbf7745..9d9f24a447 100644 --- a/http/task_service_test.go +++ b/http/task_service_test.go @@ -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" diff --git a/http/telegraf.go b/http/telegraf.go index 81b04e10a5..ef0adfb69a 100644 --- a/http/telegraf.go +++ b/http/telegraf.go @@ -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 diff --git a/http/view_service.go b/http/view_service.go index 7f9a6a16fb..91294367c0 100644 --- a/http/view_service.go +++ b/http/view_service.go @@ -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 } diff --git a/inmem/bucket_service.go b/inmem/bucket_service.go index 1051395398..f051002642 100644 --- a/inmem/bucket_service.go +++ b/inmem/bucket_service.go @@ -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 diff --git a/inmem/dashboard.go b/inmem/dashboard.go index c094d89259..6dea3884d6 100644 --- a/inmem/dashboard.go +++ b/inmem/dashboard.go @@ -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 } diff --git a/inmem/label_service.go b/inmem/label_service.go index 9111c61535..f2a3a7ccae 100644 --- a/inmem/label_service.go +++ b/inmem/label_service.go @@ -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 } diff --git a/inmem/label_test.go b/inmem/label_test.go index e3d91d8dcc..2781290d5f 100644 --- a/inmem/label_test.go +++ b/inmem/label_test.go @@ -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) } diff --git a/inmem/service.go b/inmem/service.go index ba9218d52e..cc56999d42 100644 --- a/inmem/service.go +++ b/inmem/service.go @@ -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 diff --git a/label.go b/label.go index 0676c79b53..741866626e 100644 --- a/label.go +++ b/label.go @@ -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 } diff --git a/label_test.go b/label_test.go index c6d2d924ed..9949fa74b7 100644 --- a/label_test.go +++ b/label_test.go @@ -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) diff --git a/mock/label_service.go b/mock/label_service.go index 9bdf46cf64..db4562c3a1 100644 --- a/mock/label_service.go +++ b/mock/label_service.go @@ -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) } diff --git a/testing/label_service.go b/testing/label_service.go index 017414eb11..a59a10ea1d 100644 --- a/testing/label_service.go +++ b/testing/label_service.go @@ -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) + } + }) + } +}