influxdb/http/label_service.go

746 lines
16 KiB
Go
Raw Normal View History

package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"path"
"go.uber.org/zap"
platform "github.com/influxdata/influxdb"
"github.com/julienschmidt/httprouter"
)
// 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(s platform.LabelService) *LabelHandler {
h := &LabelHandler{
Router: NewRouter(),
Logger: zap.NewNop(),
LabelService: s,
}
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",
}
}
if !b.Label.OrganizationID.Valid() {
return &platform.Error{
Code: platform.EInvalid,
Msg: "label requires a valid orgID",
}
}
return nil
}
// TODO(jm): ensure that the specified org actually exists
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, &platform.Error{
Code: platform.EInvalid,
Msg: "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, &platform.Error{
Code: platform.EInvalid,
Msg: "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 platform.Label `json:"label"`
}
func newLabelResponse(l *platform.Label) *labelResponse {
return &labelResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/labels/%s", l.ID),
},
2018-12-18 11:51:10 +00:00
Label: *l,
}
}
type labelsResponse struct {
Links map[string]string `json:"links"`
Labels []*platform.Label `json:"labels"`
}
func newLabelsResponse(ls []*platform.Label) *labelsResponse {
return &labelsResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/labels"),
},
2018-12-18 11:51:10 +00:00
Labels: ls,
}
}
// LabelBackend is all services and associated parameters required to construct
// label handlers.
type LabelBackend struct {
Logger *zap.Logger
LabelService platform.LabelService
ResourceType platform.ResourceType
}
// newGetLabelsHandler returns a handler func for a GET to /labels endpoints
func newGetLabelsHandler(b *LabelBackend) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetLabelsRequest(ctx, r, b.ResourceType)
if err != nil {
EncodeError(ctx, err, w)
return
}
labels, err := b.LabelService.FindResourceLabels(ctx, req.filter)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusOK, newLabelsResponse(labels)); err != nil {
logEncodingError(b.Logger, r, err)
return
}
}
}
type getLabelsRequest struct {
filter platform.LabelMappingFilter
}
func decodeGetLabelsRequest(ctx context.Context, r *http.Request, rt platform.ResourceType) (*getLabelsRequest, error) {
req := &getLabelsRequest{}
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
Msg: "url missing id",
}
}
var i platform.ID
if err := i.DecodeFromString(id); err != nil {
return nil, err
}
req.filter.ResourceID = i
req.filter.ResourceType = rt
return req, nil
}
// newPostLabelHandler returns a handler func for a POST to /labels endpoints
func newPostLabelHandler(b *LabelBackend) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostLabelMappingRequest(ctx, r, b.ResourceType)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := req.Mapping.Validate(); err != nil {
EncodeError(ctx, err, w)
return
}
if err := b.LabelService.CreateLabelMapping(ctx, &req.Mapping); err != nil {
EncodeError(ctx, err, w)
return
}
label, err := b.LabelService.FindLabelByID(ctx, req.Mapping.LabelID)
if err != nil {
EncodeError(ctx, err, w)
return
}
if err := encodeResponse(ctx, w, http.StatusCreated, newLabelResponse(label)); err != nil {
logEncodingError(b.Logger, r, err)
return
}
}
}
type postLabelMappingRequest struct {
Mapping platform.LabelMapping
}
func decodePostLabelMappingRequest(ctx context.Context, r *http.Request, rt platform.ResourceType) (*postLabelMappingRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
Msg: "url missing id",
}
}
var rid platform.ID
if err := rid.DecodeFromString(id); err != nil {
return nil, err
}
mapping := &platform.LabelMapping{}
if err := json.NewDecoder(r.Body).Decode(mapping); err != nil {
2019-03-29 15:52:06 +00:00
return nil, &platform.Error{
Code: platform.EInvalid,
Msg: "Invalid post label map request",
}
}
mapping.ResourceID = rid
mapping.ResourceType = rt
if err := mapping.Validate(); err != nil {
return nil, err
}
req := &postLabelMappingRequest{
Mapping: *mapping,
}
return req, nil
}
// newDeleteLabelHandler returns a handler func for a DELETE to /labels endpoints
func newDeleteLabelHandler(b *LabelBackend) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeDeleteLabelMappingRequest(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
mapping := &platform.LabelMapping{
LabelID: req.LabelID,
ResourceID: req.ResourceID,
ResourceType: b.ResourceType,
}
if err := b.LabelService.DeleteLabelMapping(ctx, mapping); err != nil {
EncodeError(ctx, err, w)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
type deleteLabelMappingRequest struct {
ResourceID platform.ID
LabelID platform.ID
}
func decodeDeleteLabelMappingRequest(ctx context.Context, r *http.Request) (*deleteLabelMappingRequest, error) {
params := httprouter.ParamsFromContext(ctx)
id := params.ByName("id")
if id == "" {
return nil, &platform.Error{
Code: platform.EInvalid,
Msg: "url missing resource id",
}
}
var rid platform.ID
if err := rid.DecodeFromString(id); err != nil {
return nil, err
}
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,
2018-12-18 09:38:49 +00:00
ResourceID: rid,
}, nil
}
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
}
defer resp.Body.Close()
2019-01-24 01:02:37 +00:00
if err := CheckError(resp); 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
}
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, err
}
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := CheckError(resp); err != nil {
return nil, err
}
var r labelsResponse
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, err
}
2018-12-18 11:51:10 +00:00
return r.Labels, nil
}
// 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
}
octets, err := json.Marshal(l)
if err != nil {
return err
}
req, err := http.NewRequest("POST", u.String(), bytes.NewReader(octets))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(u.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// TODO(jsternberg): Should this check for a 201 explicitly?
2019-01-24 01:02:37 +00:00
if err := CheckError(resp); 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
}
req.Header.Set("Content-Type", "application/json")
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if err := CheckError(resp); err != nil {
return err
}
if err := json.NewDecoder(resp.Body).Decode(m); err != nil {
return err
}
return nil
}
// 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
}
defer resp.Body.Close()
2019-01-24 01:02:37 +00:00
if err := CheckError(resp); 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
}
defer resp.Body.Close()
2019-01-24 01:02:37 +00:00
return CheckError(resp)
}
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
}
req, err := http.NewRequest("DELETE", url.String(), nil)
if err != nil {
return err
}
SetToken(s.Token, req)
hc := newClient(url.Scheme, s.InsecureSkipVerify)
resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return CheckError(resp)
}
func labelNamePath(basePath string, resourceID platform.ID, labelID platform.ID) string {
return path.Join(basePath, resourceID.String(), "labels", labelID.String())
}