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
parent
270b3c82cc
commit
e954a8063d
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue