package http import ( "context" "encoding/json" "fmt" "net/http" "path" "github.com/influxdata/httprouter" "github.com/influxdata/influxdb" pcontext "github.com/influxdata/influxdb/context" "github.com/influxdata/influxdb/kit/tracing" "github.com/influxdata/influxdb/pkg/httpc" "go.uber.org/zap" ) const prefixDocuments = "/api/v2/documents" // DocumentService is an interface HTTP-exposed portion of the document service. type DocumentService interface { CreateDocument(ctx context.Context, namespace string, orgID influxdb.ID, d *influxdb.Document) error GetDocuments(ctx context.Context, namespace string, orgID influxdb.ID) ([]*influxdb.Document, error) GetDocument(ctx context.Context, namespace string, id influxdb.ID) (*influxdb.Document, error) UpdateDocument(ctx context.Context, namespace string, d *influxdb.Document) error DeleteDocument(ctx context.Context, namespace string, id influxdb.ID) error GetDocumentLabels(ctx context.Context, namespace string, id influxdb.ID) ([]*influxdb.Label, error) AddDocumentLabel(ctx context.Context, namespace string, did influxdb.ID, lid influxdb.ID) (*influxdb.Label, error) DeleteDocumentLabel(ctx context.Context, namespace string, did influxdb.ID, lid influxdb.ID) error } // DocumentBackend is all services and associated parameters required to construct // the DocumentHandler. type DocumentBackend struct { log *zap.Logger influxdb.HTTPErrorHandler DocumentService influxdb.DocumentService LabelService influxdb.LabelService } // NewDocumentBackend returns a new instance of DocumentBackend. func NewDocumentBackend(log *zap.Logger, b *APIBackend) *DocumentBackend { return &DocumentBackend{ HTTPErrorHandler: b.HTTPErrorHandler, log: log, DocumentService: b.DocumentService, LabelService: b.LabelService, } } // DocumentHandler represents an HTTP API handler for documents. type DocumentHandler struct { *httprouter.Router log *zap.Logger influxdb.HTTPErrorHandler DocumentService influxdb.DocumentService LabelService influxdb.LabelService } const ( documentsPath = "/api/v2/documents/:ns" documentPath = "/api/v2/documents/:ns/:id" documentLabelsPath = "/api/v2/documents/:ns/:id/labels" documentLabelsIDPath = "/api/v2/documents/:ns/:id/labels/:lid" ) // NewDocumentHandler returns a new instance of DocumentHandler. // TODO(desa): this should probably take a namespace func NewDocumentHandler(b *DocumentBackend) *DocumentHandler { h := &DocumentHandler{ Router: NewRouter(b.HTTPErrorHandler), HTTPErrorHandler: b.HTTPErrorHandler, log: b.log, DocumentService: b.DocumentService, LabelService: b.LabelService, } h.HandlerFunc("POST", documentsPath, h.handlePostDocument) h.HandlerFunc("GET", documentsPath, h.handleGetDocuments) h.HandlerFunc("GET", documentPath, h.handleGetDocument) h.HandlerFunc("PUT", documentPath, h.handlePutDocument) h.HandlerFunc("DELETE", documentPath, h.handleDeleteDocument) h.HandlerFunc("GET", documentLabelsPath, h.handleGetDocumentLabel) h.HandlerFunc("POST", documentLabelsPath, h.handlePostDocumentLabel) h.HandlerFunc("DELETE", documentLabelsIDPath, h.handleDeleteDocumentLabel) return h } type documentResponse struct { Links map[string]string `json:"links"` *influxdb.Document } func newDocumentResponse(ns string, d *influxdb.Document) *documentResponse { if d.Labels == nil { d.Labels = []*influxdb.Label{} } return &documentResponse{ Links: map[string]string{ "self": fmt.Sprintf("/api/v2/documents/%s/%s", ns, d.ID), }, Document: d, } } type documentsResponse struct { Documents []*documentResponse `json:"documents"` } func newDocumentsResponse(ns string, docs []*influxdb.Document) *documentsResponse { ds := make([]*documentResponse, 0, len(docs)) for _, doc := range docs { ds = append(ds, newDocumentResponse(ns, doc)) } return &documentsResponse{ Documents: ds, } } // handlePostDocument is the HTTP handler for the POST /api/v2/documents/:ns route. func (h *DocumentHandler) handlePostDocument(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodePostDocumentRequest(ctx, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace) if err != nil { h.HandleHTTPError(ctx, err, w) return } a, err := pcontext.GetAuthorizer(ctx) if err != nil { h.HandleHTTPError(ctx, err, w) return } opts := []influxdb.DocumentOptions{} if req.OrgID.Valid() { opts = append(opts, influxdb.AuthorizedWithOrgID(a, req.OrgID)) } else { opts = append(opts, influxdb.AuthorizedWithOrg(a, req.Org)) } for _, label := range req.Labels { // TODO(desa): make these AuthorizedWithLabel eventually opts = append(opts, influxdb.WithLabel(label)) } if err := s.CreateDocument(ctx, req.Document, opts...); err != nil { h.HandleHTTPError(ctx, err, w) return } h.log.Debug("Document created", zap.String("document", fmt.Sprint(req.Document))) if err := encodeResponse(ctx, w, http.StatusCreated, newDocumentResponse(req.Namespace, req.Document)); err != nil { logEncodingError(h.log, r, err) return } } type postDocumentRequest struct { *influxdb.Document Namespace string `json:"-"` Org string `json:"org"` OrgID influxdb.ID `json:"orgID,omitempty"` Labels []influxdb.ID `json:"labels"` } func decodePostDocumentRequest(ctx context.Context, r *http.Request) (*postDocumentRequest, error) { req := &postDocumentRequest{} if err := json.NewDecoder(r.Body).Decode(req); err != nil { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "document body error", Err: err, } } if req.Document == nil { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "missing document body", } } params := httprouter.ParamsFromContext(ctx) req.Namespace = params.ByName("ns") if req.Namespace == "" { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "url missing namespace", } } return req, nil } // handleGetDocuments is the HTTP handler for the GET /api/v2/documents/:ns route. func (h *DocumentHandler) handleGetDocuments(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodeGetDocumentsRequest(ctx, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace) if err != nil { h.HandleHTTPError(ctx, err, w) return } a, err := pcontext.GetAuthorizer(ctx) if err != nil { h.HandleHTTPError(ctx, err, w) return } opts := []influxdb.DocumentFindOptions{influxdb.IncludeLabels} if req.Org != "" && req.OrgID != nil { h.HandleHTTPError(ctx, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "Please provide either org or orgID, not both", }, w) return } else if req.OrgID != nil && req.OrgID.Valid() { opt := influxdb.AuthorizedWhereOrgID(a, *req.OrgID) opts = append(opts, opt) } else if req.Org != "" { opt := influxdb.AuthorizedWhereOrg(a, req.Org) opts = append(opts, opt) } ds, err := s.FindDocuments(ctx, opts...) if err != nil { h.HandleHTTPError(ctx, err, w) return } h.log.Debug("Documents retrieved", zap.String("documents", fmt.Sprint(ds))) if err := encodeResponse(ctx, w, http.StatusOK, newDocumentsResponse(req.Namespace, ds)); err != nil { logEncodingError(h.log, r, err) return } } type getDocumentsRequest struct { Namespace string Org string OrgID *influxdb.ID } func decodeGetDocumentsRequest(ctx context.Context, r *http.Request) (*getDocumentsRequest, error) { params := httprouter.ParamsFromContext(ctx) ns := params.ByName("ns") if ns == "" { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "url missing namespace", } } qp := r.URL.Query() var oid *influxdb.ID var err error if oidStr := qp.Get("orgID"); oidStr != "" { oid, err = influxdb.IDFromString(oidStr) if err != nil { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "Invalid orgID", } } } return &getDocumentsRequest{ Namespace: ns, Org: qp.Get("org"), OrgID: oid, }, nil } func (h *DocumentHandler) handlePostDocumentLabel(w http.ResponseWriter, r *http.Request) { ctx := r.Context() _, _, err := h.getDocument(w, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } req, err := decodePostLabelMappingRequest(ctx, r, influxdb.DocumentsResourceType) if err != nil { h.HandleHTTPError(ctx, err, w) return } if err := req.Mapping.Validate(); err != nil { h.HandleHTTPError(ctx, err, w) return } if err := h.LabelService.CreateLabelMapping(ctx, &req.Mapping); err != nil { h.HandleHTTPError(ctx, err, w) return } label, err := h.LabelService.FindLabelByID(ctx, req.Mapping.LabelID) if err != nil { h.HandleHTTPError(ctx, err, w) return } h.log.Debug("Document label created", zap.String("label", fmt.Sprint(label))) if err := encodeResponse(ctx, w, http.StatusCreated, newLabelResponse(label)); err != nil { logEncodingError(h.log, r, err) return } } // handleDeleteDocumentLabel will first remove the label from the document, // then remove that label. func (h *DocumentHandler) handleDeleteDocumentLabel(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodeDeleteLabelMappingRequest(ctx, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } _, _, err = h.getDocument(w, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } _, err = h.LabelService.FindLabelByID(ctx, req.LabelID) if err != nil { h.HandleHTTPError(ctx, err, w) return } mapping := &influxdb.LabelMapping{ LabelID: req.LabelID, ResourceID: req.ResourceID, ResourceType: influxdb.DocumentsResourceType, } // remove the label if err := h.LabelService.DeleteLabelMapping(ctx, mapping); err != nil { h.HandleHTTPError(ctx, err, w) return } h.log.Debug("Document label deleted", zap.String("mapping", fmt.Sprint(mapping))) w.WriteHeader(http.StatusNoContent) } func (h *DocumentHandler) handleGetDocumentLabel(w http.ResponseWriter, r *http.Request) { ctx := r.Context() d, _, err := h.getDocument(w, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } h.log.Debug("Document label retrieved", zap.String("labels", fmt.Sprint(d.Labels))) if err := encodeResponse(ctx, w, http.StatusOK, newLabelsResponse(d.Labels)); err != nil { logEncodingError(h.log, r, err) return } } func (h *DocumentHandler) getDocument(w http.ResponseWriter, r *http.Request) (*influxdb.Document, string, error) { ctx := r.Context() req, err := decodeGetDocumentRequest(ctx, r) if err != nil { return nil, "", err } s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace) if err != nil { return nil, "", err } a, err := pcontext.GetAuthorizer(ctx) if err != nil { return nil, "", err } ds, err := s.FindDocuments(ctx, influxdb.AuthorizedWhereID(a, req.ID), influxdb.IncludeContent, influxdb.IncludeLabels) if err != nil { return nil, "", err } if len(ds) != 1 { return nil, "", &influxdb.Error{ Code: influxdb.EInternal, Msg: fmt.Sprintf("found more than one document with id %s; please report this error", req.ID), } } return ds[0], req.Namespace, nil } // handleGetDocument is the HTTP handler for the GET /api/v2/documents/:ns/:id route. func (h *DocumentHandler) handleGetDocument(w http.ResponseWriter, r *http.Request) { ctx := r.Context() d, namespace, err := h.getDocument(w, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } h.log.Debug("Document retrieved", zap.String("document", fmt.Sprint(d))) if err := encodeResponse(ctx, w, http.StatusOK, newDocumentResponse(namespace, d)); err != nil { logEncodingError(h.log, r, err) return } } type getDocumentRequest struct { Namespace string ID influxdb.ID } func decodeGetDocumentRequest(ctx context.Context, r *http.Request) (*getDocumentRequest, error) { params := httprouter.ParamsFromContext(ctx) ns := params.ByName("ns") if ns == "" { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "url missing namespace", } } i := params.ByName("id") if i == "" { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "url missing id", } } var id influxdb.ID if err := id.DecodeFromString(i); err != nil { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "bad id in url", } } return &getDocumentRequest{ Namespace: ns, ID: id, }, nil } // handleDeleteDocument is the HTTP handler for the DELETE /api/v2/documents/:ns/:id route. func (h *DocumentHandler) handleDeleteDocument(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodeDeleteDocumentRequest(ctx, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace) if err != nil { h.HandleHTTPError(ctx, err, w) return } a, err := pcontext.GetAuthorizer(ctx) if err != nil { h.HandleHTTPError(ctx, err, w) return } if err := s.DeleteDocuments(ctx, influxdb.AuthorizedWhereID(a, req.ID)); err != nil { h.HandleHTTPError(ctx, err, w) return } h.log.Debug("Document deleted", zap.String("documentID", fmt.Sprint(req.ID))) w.WriteHeader(http.StatusNoContent) } type deleteDocumentRequest struct { Namespace string ID influxdb.ID } func decodeDeleteDocumentRequest(ctx context.Context, r *http.Request) (*deleteDocumentRequest, error) { params := httprouter.ParamsFromContext(ctx) ns := params.ByName("ns") if ns == "" { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "url missing namespace", } } i := params.ByName("id") if i == "" { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "url missing id", } } var id influxdb.ID if err := id.DecodeFromString(i); err != nil { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "bad id in url", } } return &deleteDocumentRequest{ Namespace: ns, ID: id, }, nil } // handlePutDocument is the HTTP handler for the PUT /api/v2/documents/:ns/:id route. func (h *DocumentHandler) handlePutDocument(w http.ResponseWriter, r *http.Request) { ctx := r.Context() req, err := decodePutDocumentRequest(ctx, r) if err != nil { h.HandleHTTPError(ctx, err, w) return } s, err := h.DocumentService.FindDocumentStore(ctx, req.Namespace) if err != nil { h.HandleHTTPError(ctx, err, w) return } a, err := pcontext.GetAuthorizer(ctx) if err != nil { h.HandleHTTPError(ctx, err, w) return } if err := s.UpdateDocument(ctx, req.Document, influxdb.Authorized(a)); err != nil { h.HandleHTTPError(ctx, err, w) return } ds, err := s.FindDocuments(ctx, influxdb.WhereID(req.Document.ID), influxdb.IncludeContent, influxdb.IncludeLabels) if err != nil { h.HandleHTTPError(ctx, err, w) return } if len(ds) != 1 { err := &influxdb.Error{ Code: influxdb.EInternal, Msg: fmt.Sprintf("found more than one document with id %s; please report this error", req.ID), } h.HandleHTTPError(ctx, err, w) return } d := ds[0] h.log.Debug("Document updated", zap.String("document", fmt.Sprint(d))) if err := encodeResponse(ctx, w, http.StatusOK, newDocumentResponse(req.Namespace, d)); err != nil { logEncodingError(h.log, r, err) return } } type putDocumentRequest struct { *influxdb.Document Namespace string `json:"-"` } func decodePutDocumentRequest(ctx context.Context, r *http.Request) (*putDocumentRequest, error) { req := &putDocumentRequest{} if err := json.NewDecoder(r.Body).Decode(req); err != nil { return nil, err } params := httprouter.ParamsFromContext(ctx) i := params.ByName("id") if i == "" { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "url missing id", } } if err := req.ID.DecodeFromString(i); err != nil { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Err: err, } } req.Namespace = params.ByName("ns") if req.Namespace == "" { return nil, &influxdb.Error{ Code: influxdb.EInvalid, Msg: "url missing namespace", } } return req, nil } type documentService struct { Client *httpc.Client } // NewDocumentService creates a client to connect to Influx via HTTP to manage documents. func NewDocumentService(client *httpc.Client) DocumentService { return &documentService{ Client: client, } } func buildDocumentsPath(namespace string) string { return path.Join(prefixDocuments, namespace) } func buildDocumentPath(namespace string, id influxdb.ID) string { return path.Join(prefixDocuments, namespace, id.String()) } func buildDocumentLabelsPath(namespace string, id influxdb.ID) string { return path.Join(prefixDocuments, namespace, id.String(), "labels") } func buildDocumentLabelPath(namespace string, did influxdb.ID, lid influxdb.ID) string { return path.Join(prefixDocuments, namespace, did.String(), "labels", lid.String()) } // CreateDocument creates a document in the specified namespace. // Only the ids of the given labels will be used. // After the call, if successful, the input document will contain the new one. func (s *documentService) CreateDocument(ctx context.Context, namespace string, orgID influxdb.ID, d *influxdb.Document) error { span, _ := tracing.StartSpanFromContext(ctx) defer span.Finish() lids := make([]influxdb.ID, len(d.Labels)) for i := 0; i < len(lids); i++ { lids[i] = d.Labels[i].ID } // Set a valid ID for proper marshaling. // It will be assigned by the backend in any case. d.ID = influxdb.ID(1) req := &postDocumentRequest{ Document: d, OrgID: orgID, Labels: lids, } var resp documentResponse if err := s.Client. PostJSON(req, buildDocumentsPath(namespace)). DecodeJSON(&resp). Do(ctx); err != nil { return err } *d = *resp.Document return nil } // GetDocuments returns the documents for a `namespace` and an `orgID`. // Returned documents do not contain their content. func (s *documentService) GetDocuments(ctx context.Context, namespace string, orgID influxdb.ID) ([]*influxdb.Document, error) { span, _ := tracing.StartSpanFromContext(ctx) defer span.Finish() var resp documentsResponse r := s.Client. Get(buildDocumentsPath(namespace)). DecodeJSON(&resp) r = r.QueryParams([2]string{"orgID", orgID.String()}) if err := r.Do(ctx); err != nil { return nil, err } docs := make([]*influxdb.Document, len(resp.Documents)) for i := 0; i < len(docs); i++ { docs[i] = resp.Documents[i].Document } return docs, nil } // GetDocument returns the document with the specified id. func (s *documentService) GetDocument(ctx context.Context, namespace string, id influxdb.ID) (*influxdb.Document, error) { span, _ := tracing.StartSpanFromContext(ctx) defer span.Finish() var resp documentResponse if err := s.Client. Get(buildDocumentPath(namespace, id)). DecodeJSON(&resp). Do(ctx); err != nil { return nil, err } return resp.Document, nil } // UpdateDocument updates the document with id `d.ID` and replaces the content of `d` with the patched value. func (s *documentService) UpdateDocument(ctx context.Context, namespace string, d *influxdb.Document) error { span, _ := tracing.StartSpanFromContext(ctx) defer span.Finish() var resp documentResponse if err := s.Client. PutJSON(d, buildDocumentPath(namespace, d.ID)). DecodeJSON(&resp). Do(ctx); err != nil { return err } *d = *resp.Document return nil } // DeleteDocument deletes the document with the given id. func (s *documentService) DeleteDocument(ctx context.Context, namespace string, id influxdb.ID) error { span, _ := tracing.StartSpanFromContext(ctx) defer span.Finish() if err := s.Client. Delete(buildDocumentPath(namespace, id)). Do(ctx); err != nil { return err } return nil } // GetDocumentLabels returns the labels associated to the document with the given id. func (s *documentService) GetDocumentLabels(ctx context.Context, namespace string, id influxdb.ID) ([]*influxdb.Label, error) { span, _ := tracing.StartSpanFromContext(ctx) defer span.Finish() var resp labelsResponse if err := s.Client. Get(buildDocumentLabelsPath(namespace, id)). DecodeJSON(&resp). Do(ctx); err != nil { return nil, err } return resp.Labels, nil } // AddDocumentLabel adds the label with id `lid` to the document with id `did`. func (s *documentService) AddDocumentLabel(ctx context.Context, namespace string, did influxdb.ID, lid influxdb.ID) (*influxdb.Label, error) { span, _ := tracing.StartSpanFromContext(ctx) defer span.Finish() mapping := &influxdb.LabelMapping{ LabelID: lid, } var resp labelResponse if err := s.Client. PostJSON(mapping, buildDocumentLabelsPath(namespace, did)). DecodeJSON(&resp). Do(ctx); err != nil { return nil, err } return &resp.Label, nil } // DeleteDocumentLabel deletes the label with id `lid` from the document with id `did`. func (s *documentService) DeleteDocumentLabel(ctx context.Context, namespace string, did influxdb.ID, lid influxdb.ID) error { span, _ := tracing.StartSpanFromContext(ctx) defer span.Finish() if err := s.Client. Delete(buildDocumentLabelPath(namespace, did, lid)). Do(ctx); err != nil { return err } return nil }