feat(platform): add ability to query sources

Currently all  that is supported is v1 sources. This code will likely
need revisiting.

Co-authored-by: Andrew Watkins <andrew.watkinz@gmail.com>
Co-authored-by: Michael Desa <mjdesa@gmail.com>
pull/10616/head
Michael Desa 2018-07-31 18:50:02 -04:00 committed by Andrew Watkins
parent 270b3c82cc
commit e954a8063d
7 changed files with 216 additions and 9 deletions

View File

@ -270,6 +270,9 @@ func (c *Client) setServices(ctx context.Context, s *platform.Source) error {
s.BucketService = &influxdb.BucketService{ s.BucketService = &influxdb.BucketService{
Source: s, Source: s,
} }
s.SourceQuerier = &influxdb.SourceQuerier{
Source: s,
}
default: default:
return fmt.Errorf("unsupported source type %s", s.Type) return fmt.Errorf("unsupported source type %s", s.Type)
} }

56
http/influxdb/query.go Normal file
View File

@ -0,0 +1,56 @@
package influxdb
import (
"bytes"
"context"
"fmt"
"github.com/influxdata/platform"
"github.com/influxdata/platform/chronograf"
)
// SourceQuerier connects to Influx via HTTP using tokens to manage buckets
type SourceQuerier struct {
Source *platform.Source
}
func (s *SourceQuerier) Query(ctx context.Context, q *platform.SourceQuery) (*platform.SourceQueryResult, error) {
switch q.Type {
case "influxql":
return s.influxQuery(ctx, q)
case "flux":
return s.fluxQuery(ctx, q)
}
return nil, fmt.Errorf("unsupport language %v", q.Type)
}
func (s *SourceQuerier) influxQuery(ctx context.Context, q *platform.SourceQuery) (*platform.SourceQueryResult, error) {
c, err := newClient(s.Source)
if err != nil {
return nil, err
}
query := chronograf.Query{
Command: q.Query,
// TODO(specify database)
}
res, err := c.Query(ctx, query)
if err != nil {
return nil, err
}
b, err := res.MarshalJSON()
if err != nil {
return nil, err
}
return &platform.SourceQueryResult{
Reader: bytes.NewReader(b),
}, nil
}
func (s *SourceQuerier) fluxQuery(ctx context.Context, q *platform.SourceQuery) (*platform.SourceQueryResult, error) {
panic("not implemented")
}

View File

@ -30,6 +30,18 @@ func setCORSResponseHeaders(w nethttp.ResponseWriter, r *nethttp.Request) {
} }
} }
var platformLinks = map[string]interface{}{
"sources": "/v2/sources",
}
func (h *PlatformHandler) serveLinks(w nethttp.ResponseWriter, r *nethttp.Request) {
ctx := r.Context()
if err := encodeResponse(ctx, w, nethttp.StatusOK, platformLinks); err != nil {
EncodeError(ctx, err, w)
return
}
}
// ServeHTTP delegates a request to the appropriate subhandler. // ServeHTTP delegates a request to the appropriate subhandler.
func (h *PlatformHandler) ServeHTTP(w nethttp.ResponseWriter, r *nethttp.Request) { func (h *PlatformHandler) ServeHTTP(w nethttp.ResponseWriter, r *nethttp.Request) {
@ -40,13 +52,18 @@ func (h *PlatformHandler) ServeHTTP(w nethttp.ResponseWriter, r *nethttp.Request
// Server the chronograf assets for any basepath that does not start with addressable parts // Server the chronograf assets for any basepath that does not start with addressable parts
// of the platform API. // of the platform API.
if !strings.HasPrefix(r.URL.Path, "/v1/") && if !strings.HasPrefix(r.URL.Path, "/v1") &&
!strings.HasPrefix(r.URL.Path, "/v2/") && !strings.HasPrefix(r.URL.Path, "/v2") &&
!strings.HasPrefix(r.URL.Path, "/chronograf/") { !strings.HasPrefix(r.URL.Path, "/chronograf/") {
h.AssetHandler.ServeHTTP(w, r) h.AssetHandler.ServeHTTP(w, r)
return return
} }
if r.URL.Path == "/v2" {
h.serveLinks(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/chronograf/") { if strings.HasPrefix(r.URL.Path, "/chronograf/") {
h.ChronografHandler.ServeHTTP(w, r) h.ChronografHandler.ServeHTTP(w, r)
return return
@ -55,8 +72,8 @@ func (h *PlatformHandler) ServeHTTP(w nethttp.ResponseWriter, r *nethttp.Request
ctx := r.Context() ctx := r.Context()
var err error var err error
if ctx, err = extractAuthorization(ctx, r); err != nil { if ctx, err = extractAuthorization(ctx, r); err != nil {
nethttp.Error(w, err.Error(), nethttp.StatusBadRequest) // TODO(desa): add back eventually when things have settled
return //nethttp.Error(w, err.Error(), nethttp.StatusBadRequest)
} }
r = r.WithContext(ctx) r = r.WithContext(ctx)

View File

@ -4,17 +4,64 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io"
"net/http" "net/http"
"path" "path"
"go.uber.org/zap"
"github.com/influxdata/platform" "github.com/influxdata/platform"
kerrors "github.com/influxdata/platform/kit/errors" kerrors "github.com/influxdata/platform/kit/errors"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
const (
sourceHTTPPath = "/v2/sources"
)
type sourceResponse struct {
*platform.Source
Links map[string]interface{} `json:"links"`
}
func newSourceResponse(s *platform.Source) *sourceResponse {
s.Password = ""
s.SharedSecret = ""
return &sourceResponse{
Source: s,
Links: map[string]interface{}{
"self": fmt.Sprintf("%s/%s", sourceHTTPPath, s.ID.String()),
"query": fmt.Sprintf("%s/%s/query", sourceHTTPPath, s.ID.String()),
"buckets": fmt.Sprintf("%s/%s/buckets", sourceHTTPPath, s.ID.String()),
},
}
}
type sourcesResponse struct {
Sources []*sourceResponse `json:"sources"`
Links map[string]interface{} `json:"links"`
}
func newSourcesResponse(srcs []*platform.Source) *sourcesResponse {
res := &sourcesResponse{
Links: map[string]interface{}{
"self": sourceHTTPPath,
},
}
res.Sources = make([]*sourceResponse, 0, len(srcs))
for _, src := range srcs {
res.Sources = append(res.Sources, newSourceResponse(src))
}
return res
}
// SourceHandler is a handler for sources // SourceHandler is a handler for sources
type SourceHandler struct { type SourceHandler struct {
*httprouter.Router *httprouter.Router
Logger *zap.Logger
SourceService platform.SourceService SourceService platform.SourceService
} }
@ -23,6 +70,7 @@ type SourceHandler struct {
func NewSourceHandler() *SourceHandler { func NewSourceHandler() *SourceHandler {
h := &SourceHandler{ h := &SourceHandler{
Router: httprouter.New(), Router: httprouter.New(),
Logger: zap.NewNop(),
} }
h.HandlerFunc("POST", "/v2/sources", h.handlePostSource) h.HandlerFunc("POST", "/v2/sources", h.handlePostSource)
@ -32,9 +80,61 @@ func NewSourceHandler() *SourceHandler {
h.HandlerFunc("DELETE", "/v2/sources/:id", h.handleDeleteSource) h.HandlerFunc("DELETE", "/v2/sources/:id", h.handleDeleteSource)
h.HandlerFunc("GET", "/v2/sources/:id/buckets", h.handleGetSourcesBuckets) h.HandlerFunc("GET", "/v2/sources/:id/buckets", h.handleGetSourcesBuckets)
h.HandlerFunc("POST", "/v2/sources/:id/query", h.handlePostSourceQuery)
return h return h
} }
// handlePostSourceQuery is the HTTP handler for POST /v2/sources/:id/query
func (h *SourceHandler) handlePostSourceQuery(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostSourceQuery(ctx, r)
if err != nil {
EncodeError(ctx, err, w)
return
}
s, err := h.SourceService.FindSourceByID(ctx, req.SourceID)
if err != nil {
EncodeError(ctx, err, w)
return
}
res, err := s.SourceQuerier.Query(ctx, req.sourceQuery)
if err != nil {
EncodeError(ctx, err, w)
return
}
w.WriteHeader(http.StatusOK)
if _, err := io.Copy(w, res.Reader); err != nil {
h.Logger.Info("error copying query response", zap.Error(err))
}
}
type postSourceQuery struct {
*getSourceRequest
sourceQuery *platform.SourceQuery
}
func decodePostSourceQuery(ctx context.Context, r *http.Request) (*postSourceQuery, error) {
getSrcReq, err := decodeGetSourceRequest(ctx, r)
if err != nil {
return nil, err
}
q := &platform.SourceQuery{}
if err := json.NewDecoder(r.Body).Decode(q); err != nil {
return nil, err
}
return &postSourceQuery{
getSourceRequest: getSrcReq,
sourceQuery: q,
}, nil
}
// handleGetSourcesBuckets is the HTTP handler for the GET /v2/sources/:id/buckets route. // handleGetSourcesBuckets is the HTTP handler for the GET /v2/sources/:id/buckets route.
func (h *SourceHandler) handleGetSourcesBuckets(w http.ResponseWriter, r *http.Request) { func (h *SourceHandler) handleGetSourcesBuckets(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -130,13 +230,15 @@ func (h *SourceHandler) handleGetSource(w http.ResponseWriter, r *http.Request)
return return
} }
b, err := h.SourceService.FindSourceByID(ctx, req.SourceID) s, err := h.SourceService.FindSourceByID(ctx, req.SourceID)
if err != nil { if err != nil {
EncodeError(ctx, err, w) EncodeError(ctx, err, w)
return return
} }
if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil { res := newSourceResponse(s)
if err := encodeResponse(ctx, w, http.StatusOK, res); err != nil {
EncodeError(ctx, err, w) EncodeError(ctx, err, w)
return return
} }
@ -214,13 +316,15 @@ func (h *SourceHandler) handleGetSources(w http.ResponseWriter, r *http.Request)
return return
} }
bs, _, err := h.SourceService.FindSources(ctx, req.findOptions) srcs, _, err := h.SourceService.FindSources(ctx, req.findOptions)
if err != nil { if err != nil {
EncodeError(ctx, err, w) EncodeError(ctx, err, w)
return return
} }
if err := encodeResponse(ctx, w, http.StatusOK, bs); err != nil { res := newSourcesResponse(srcs)
if err := encodeResponse(ctx, w, http.StatusOK, res); err != nil {
EncodeError(ctx, err, w) EncodeError(ctx, err, w)
return return
} }

25
query.go Normal file
View File

@ -0,0 +1,25 @@
package platform
import (
"context"
"io"
)
// TODO(desa): These files are possibly a temporary. This is needed
// as a part of the source work that is being done.
// SourceQuery is a query for a source.
type SourceQuery struct {
Query string `json:"query"`
Type string `json:"type"`
}
// SourceQueryResult is a result of a source query.
type SourceQueryResult struct {
Reader io.Reader
}
// SourceQuerier allows for the querying of sources.
type SourceQuerier interface {
Query(ctx context.Context, q *SourceQuery) (*SourceQueryResult, error)
}

View File

@ -40,6 +40,8 @@ type Source struct {
V1SourceFields V1SourceFields
BucketService BucketService `json:"-"` BucketService BucketService `json:"-"`
// TODO(desa): is this a good idea?
SourceQuerier SourceQuerier `json:"-"`
} }
// V1SourceFields are the fields for connecting to a 1.0 source (oss or enterprise) // V1SourceFields are the fields for connecting to a 1.0 source (oss or enterprise)

View File

@ -24,7 +24,7 @@ var sourceCmpOptions = cmp.Options{
cmp.Comparer(func(x, y []byte) bool { cmp.Comparer(func(x, y []byte) bool {
return bytes.Equal(x, y) return bytes.Equal(x, y)
}), }),
cmpopts.IgnoreFields(platform.Source{}, "BucketService"), cmpopts.IgnoreFields(platform.Source{}, "SourceQuerier", "BucketService"),
cmp.Transformer("Sort", func(in []*platform.Source) []*platform.Source { cmp.Transformer("Sort", func(in []*platform.Source) []*platform.Source {
out := append([]*platform.Source(nil), in...) // Copy input to avoid mutating it out := append([]*platform.Source(nil), in...) // Copy input to avoid mutating it
sort.Slice(out, func(i, j int) bool { sort.Slice(out, func(i, j int) bool {