397 lines
9.4 KiB
Go
397 lines
9.4 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"path"
|
|
|
|
"github.com/influxdata/platform"
|
|
kerrors "github.com/influxdata/platform/kit/errors"
|
|
"github.com/julienschmidt/httprouter"
|
|
)
|
|
|
|
// ScraperHandler represents an HTTP API handler for scraper targets.
|
|
type ScraperHandler struct {
|
|
*httprouter.Router
|
|
ScraperStorageService platform.ScraperTargetStoreService
|
|
}
|
|
|
|
const (
|
|
targetPath = "/api/v2/scrapertargets"
|
|
)
|
|
|
|
// NewScraperHandler returns a new instance of ScraperHandler.
|
|
func NewScraperHandler() *ScraperHandler {
|
|
h := &ScraperHandler{
|
|
Router: httprouter.New(),
|
|
}
|
|
h.HandlerFunc("POST", targetPath, h.handlePostScraperTarget)
|
|
h.HandlerFunc("GET", targetPath, h.handleGetScraperTargets)
|
|
h.HandlerFunc("GET", targetPath+"/:id", h.handleGetScraperTarget)
|
|
h.HandlerFunc("PATCH", targetPath+"/:id", h.handlePatchScraperTarget)
|
|
h.HandlerFunc("DELETE", targetPath+"/:id", h.handleDeleteScraperTarget)
|
|
return h
|
|
}
|
|
|
|
// handlePostScraperTarget is HTTP handler for the POST /api/v2/scrapertargets route.
|
|
func (h *ScraperHandler) handlePostScraperTarget(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
req, err := decodeScraperTargetAddRequest(ctx, r)
|
|
if err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
|
|
if err := h.ScraperStorageService.AddTarget(ctx, req); err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
if err := encodeResponse(ctx, w, http.StatusCreated, newTargetResponse(*req)); err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
}
|
|
|
|
// handleDeleteScraperTarget is the HTTP handler for the DELETE /api/v2/scrapertargets/:id route.
|
|
func (h *ScraperHandler) handleDeleteScraperTarget(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
id, err := decodeScraperTargetIDRequest(ctx, r)
|
|
if err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
|
|
if err := h.ScraperStorageService.RemoveTarget(ctx, *id); err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}
|
|
|
|
// handlePatchScraperTarget is the HTTP handler for the PATCH /api/v2/scrapertargets/:id route.
|
|
func (h *ScraperHandler) handlePatchScraperTarget(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
update, err := decodeScraperTargetUpdateRequest(ctx, r)
|
|
if err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
|
|
target, err := h.ScraperStorageService.UpdateTarget(ctx, update)
|
|
if err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
|
|
if err := encodeResponse(ctx, w, http.StatusOK, newTargetResponse(*target)); err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *ScraperHandler) handleGetScraperTarget(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
id, err := decodeScraperTargetIDRequest(ctx, r)
|
|
if err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
target, err := h.ScraperStorageService.GetTargetByID(ctx, *id)
|
|
if err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
|
|
if err := encodeResponse(ctx, w, http.StatusOK, newTargetResponse(*target)); err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
}
|
|
|
|
// handleGetScraperTargets is the HTTP handler for the GET /api/v2/scrapertargets route.
|
|
func (h *ScraperHandler) handleGetScraperTargets(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
targets, err := h.ScraperStorageService.ListTargets(ctx)
|
|
if err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
|
|
if err := encodeResponse(ctx, w, http.StatusOK, newListTargetsResponse(targets)); err != nil {
|
|
EncodeError(ctx, err, w)
|
|
return
|
|
}
|
|
}
|
|
|
|
func decodeScraperTargetUpdateRequest(ctx context.Context, r *http.Request) (
|
|
*platform.ScraperTarget, error) {
|
|
update := &platform.ScraperTarget{}
|
|
if err := json.NewDecoder(r.Body).Decode(update); err != nil {
|
|
return nil, err
|
|
}
|
|
id, err := decodeScraperTargetIDRequest(ctx, r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
update.ID = *id
|
|
return update, nil
|
|
}
|
|
|
|
func decodeScraperTargetAddRequest(ctx context.Context, r *http.Request) (*platform.ScraperTarget, error) {
|
|
req := &platform.ScraperTarget{}
|
|
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
|
return nil, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func decodeScraperTargetIDRequest(ctx context.Context, r *http.Request) (*platform.ID, error) {
|
|
params := httprouter.ParamsFromContext(ctx)
|
|
id := params.ByName("id")
|
|
if id == "" {
|
|
return nil, kerrors.InvalidDataf("url missing id")
|
|
}
|
|
|
|
var i platform.ID
|
|
if err := i.DecodeFromString(id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &i, nil
|
|
}
|
|
|
|
// ScraperService connects to Influx via HTTP using tokens to manage scraper targets.
|
|
type ScraperService struct {
|
|
Addr string
|
|
Token string
|
|
InsecureSkipVerify bool
|
|
}
|
|
|
|
// ListTargets returns a list of all scraper targets.
|
|
func (s *ScraperService) ListTargets(ctx context.Context) ([]platform.ScraperTarget, error) {
|
|
url, err := newURL(s.Addr, targetPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := url.Query()
|
|
|
|
req, err := http.NewRequest("GET", url.String(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.URL.RawQuery = query.Encode()
|
|
SetToken(s.Token, req)
|
|
|
|
hc := newClient(url.Scheme, s.InsecureSkipVerify)
|
|
resp, err := hc.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := CheckError(resp); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var targetsResp getTargetsResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&targetsResp); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
targets := make([]platform.ScraperTarget, len(targetsResp.Targets))
|
|
for k, v := range targetsResp.Targets {
|
|
targets[k] = v.ScraperTarget
|
|
}
|
|
|
|
return targets, nil
|
|
}
|
|
|
|
// UpdateTarget updates a single scraper target with changeset.
|
|
// Returns the new target state after update.
|
|
func (s *ScraperService) UpdateTarget(ctx context.Context, update *platform.ScraperTarget) (*platform.ScraperTarget, error) {
|
|
if !update.ID.Valid() {
|
|
return nil, errors.New("update scraper: id is invalid")
|
|
}
|
|
url, err := newURL(s.Addr, targetIDPath(update.ID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
octets, err := json.Marshal(update)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequest("PATCH", url.String(), bytes.NewReader(octets))
|
|
if err != nil {
|
|
return nil, 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 nil, err
|
|
}
|
|
|
|
if err := CheckError(resp); err != nil {
|
|
return nil, err
|
|
}
|
|
var targetResp targetResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&targetResp); err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return &targetResp.ScraperTarget, nil
|
|
}
|
|
|
|
// AddTarget creates a new scraper target and sets target.ID with the new identifier.
|
|
func (s *ScraperService) AddTarget(ctx context.Context, target *platform.ScraperTarget) error {
|
|
url, err := newURL(s.Addr, targetPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
octets, err := json.Marshal(target)
|
|
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
|
|
}
|
|
|
|
// TODO(jsternberg): Should this check for a 201 explicitly?
|
|
if err := CheckError(resp); err != nil {
|
|
return err
|
|
}
|
|
|
|
targetResp := new(targetResponse)
|
|
if err := json.NewDecoder(resp.Body).Decode(targetResp); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveTarget removes a scraper target by ID.
|
|
func (s *ScraperService) RemoveTarget(ctx context.Context, id platform.ID) error {
|
|
url, err := newURL(s.Addr, targetIDPath(id))
|
|
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
|
|
}
|
|
return CheckError(resp)
|
|
}
|
|
|
|
// GetTargetByID returns a single target by ID.
|
|
func (s *ScraperService) GetTargetByID(ctx context.Context, id platform.ID) (*platform.ScraperTarget, error) {
|
|
url, err := newURL(s.Addr, targetIDPath(id))
|
|
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
|
|
}
|
|
|
|
if err := CheckError(resp); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var targetResp targetResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&targetResp); err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return &targetResp.ScraperTarget, nil
|
|
}
|
|
|
|
func targetIDPath(id platform.ID) string {
|
|
return path.Join(targetPath, id.String())
|
|
}
|
|
|
|
type getTargetsLinks struct {
|
|
Self string `json:"self"`
|
|
}
|
|
|
|
type getTargetsResponse struct {
|
|
Links getTargetsLinks `json:"links"`
|
|
Targets []targetResponse `json:"scraper_targets"`
|
|
}
|
|
|
|
type targetLinks struct {
|
|
Self string `json:"self"`
|
|
}
|
|
|
|
type targetResponse struct {
|
|
platform.ScraperTarget
|
|
Links targetLinks `json:"links"`
|
|
}
|
|
|
|
func newListTargetsResponse(targets []platform.ScraperTarget) getTargetsResponse {
|
|
res := getTargetsResponse{
|
|
Links: getTargetsLinks{
|
|
Self: targetPath,
|
|
},
|
|
Targets: make([]targetResponse, 0, len(targets)),
|
|
}
|
|
|
|
for _, target := range targets {
|
|
res.Targets = append(res.Targets, newTargetResponse(target))
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func newTargetResponse(target platform.ScraperTarget) targetResponse {
|
|
return targetResponse{
|
|
Links: targetLinks{
|
|
Self: targetIDPath(target.ID),
|
|
},
|
|
ScraperTarget: target,
|
|
}
|
|
}
|