Merge pull request #12978 from influxdata/varible-labels

feat(http): add labels to variables
pull/13032/head
Jade McGough 2019-03-29 17:50:14 -07:00 committed by GitHub
commit 5fbed0b6a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 297 additions and 21 deletions

View File

@ -1262,6 +1262,98 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/variables/{variableID}/labels':
get:
tags:
- Variables
summary: list all labels for a variable
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: variableID
schema:
type: string
required: true
description: ID of the variable
responses:
'200':
description: a list of all labels for a variable
content:
application/json:
schema:
$ref: "#/components/schemas/LabelsResponse"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
tags:
- Variables
summary: add a label to a variable
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: variableID
schema:
type: string
required: true
description: ID of the variable
requestBody:
description: label to add
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LabelMapping"
responses:
'200':
description: a list of all labels for a variable
content:
application/json:
schema:
$ref: "#/components/schemas/LabelsResponse"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/variables/{variableID}/labels/{labelID}':
delete:
tags:
- Variables
summary: delete a label from a variable
parameters:
- $ref: '#/components/parameters/TraceSpan'
- in: path
name: variableID
schema:
type: string
required: true
description: ID of the variable
- in: path
name: labelID
schema:
type: string
required: true
description: the label id to delete
responses:
'204':
description: delete has been accepted
'404':
description: variable not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/write:
post:
tags:
@ -6467,6 +6559,9 @@ components:
org:
type: string
format: uri
labels:
type: string
format: uri
id:
readOnly: true
type: string
@ -6478,6 +6573,8 @@ components:
type: array
items:
type: string
labels:
$ref: "#/components/schemas/Labels"
arguments:
type: object
oneOf:

View File

@ -22,12 +22,15 @@ const (
type VariableBackend struct {
Logger *zap.Logger
VariableService platform.VariableService
LabelService platform.LabelService
}
// NewVariableBackend creates a backend used by the variable handler.
func NewVariableBackend(b *APIBackend) *VariableBackend {
return &VariableBackend{
Logger: b.Logger.With(zap.String("handler", "variable")),
VariableService: b.VariableService,
LabelService: b.LabelService,
}
}
@ -38,6 +41,7 @@ type VariableHandler struct {
Logger *zap.Logger
VariableService platform.VariableService
LabelService platform.LabelService
}
// NewVariableHandler creates a new VariableHandler
@ -47,9 +51,12 @@ func NewVariableHandler(b *VariableBackend) *VariableHandler {
Logger: b.Logger,
VariableService: b.VariableService,
LabelService: b.LabelService,
}
entityPath := fmt.Sprintf("%s/:id", variablePath)
entityLabelsPath := fmt.Sprintf("%s/labels", entityPath)
entityLabelsIDPath := fmt.Sprintf("%s/:lid", entityLabelsPath)
h.HandlerFunc("GET", variablePath, h.handleGetVariables)
h.HandlerFunc("POST", variablePath, h.handlePostVariable)
@ -58,6 +65,15 @@ func NewVariableHandler(b *VariableBackend) *VariableHandler {
h.HandlerFunc("PUT", entityPath, h.handlePutVariable)
h.HandlerFunc("DELETE", entityPath, h.handleDeleteVariable)
labelBackend := &LabelBackend{
Logger: b.Logger.With(zap.String("handler", "label")),
LabelService: b.LabelService,
ResourceType: platform.DashboardsResourceType,
}
h.HandlerFunc("GET", entityLabelsPath, newGetLabelsHandler(labelBackend))
h.HandlerFunc("POST", entityLabelsPath, newPostLabelHandler(labelBackend))
h.HandlerFunc("DELETE", entityLabelsIDPath, newDeleteLabelHandler(labelBackend))
return h
}
@ -74,7 +90,7 @@ func (r getVariablesResponse) ToPlatform() []*platform.Variable {
return variables
}
func newGetVariablesResponse(variables []*platform.Variable, f platform.VariableFilter, opts platform.FindOptions) getVariablesResponse {
func newGetVariablesResponse(ctx context.Context, variables []*platform.Variable, f platform.VariableFilter, opts platform.FindOptions, labelService platform.LabelService) getVariablesResponse {
num := len(variables)
resp := getVariablesResponse{
Variables: make([]variableResponse, 0, num),
@ -82,7 +98,8 @@ func newGetVariablesResponse(variables []*platform.Variable, f platform.Variable
}
for _, variable := range variables {
resp.Variables = append(resp.Variables, newVariableResponse(variable))
labels, _ := labelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: variable.ID})
resp.Variables = append(resp.Variables, newVariableResponse(variable, labels))
}
return resp
@ -138,7 +155,7 @@ func (h *VariableHandler) handleGetVariables(w http.ResponseWriter, r *http.Requ
return
}
err = encodeResponse(ctx, w, http.StatusOK, newGetVariablesResponse(variables, req.filter, req.opts))
err = encodeResponse(ctx, w, http.StatusOK, newGetVariablesResponse(ctx, variables, req.filter, req.opts, h.LabelService))
if err != nil {
logEncodingError(h.Logger, r, err)
return
@ -181,7 +198,13 @@ func (h *VariableHandler) handleGetVariable(w http.ResponseWriter, r *http.Reque
return
}
err = encodeResponse(ctx, w, http.StatusOK, newVariableResponse(variable))
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: variable.ID})
if err != nil {
EncodeError(ctx, err, w)
return
}
err = encodeResponse(ctx, w, http.StatusOK, newVariableResponse(variable, labels))
if err != nil {
logEncodingError(h.Logger, r, err)
return
@ -189,23 +212,33 @@ func (h *VariableHandler) handleGetVariable(w http.ResponseWriter, r *http.Reque
}
type variableLinks struct {
Self string `json:"self"`
Org string `json:"org"`
Self string `json:"self"`
Labels string `json:"labels"`
Org string `json:"org"`
}
type variableResponse struct {
*platform.Variable
Links variableLinks `json:"links"`
Labels []platform.Label `json:"labels"`
Links variableLinks `json:"links"`
}
func newVariableResponse(m *platform.Variable) variableResponse {
return variableResponse{
func newVariableResponse(m *platform.Variable, labels []*platform.Label) variableResponse {
res := variableResponse{
Variable: m,
Labels: []platform.Label{},
Links: variableLinks{
Self: fmt.Sprintf("/api/v2/variables/%s", m.ID),
Org: fmt.Sprintf("/api/v2/orgs/%s", m.OrganizationID),
Self: fmt.Sprintf("/api/v2/variables/%s", m.ID),
Labels: fmt.Sprintf("/api/v2/variables/%s/labels", m.ID),
Org: fmt.Sprintf("/api/v2/orgs/%s", m.OrganizationID),
},
}
for _, l := range labels {
res.Labels = append(res.Labels, *l)
}
return res
}
func (h *VariableHandler) handlePostVariable(w http.ResponseWriter, r *http.Request) {
@ -223,8 +256,7 @@ func (h *VariableHandler) handlePostVariable(w http.ResponseWriter, r *http.Requ
return
}
err = encodeResponse(ctx, w, http.StatusCreated, newVariableResponse(req.variable))
if err != nil {
if err := encodeResponse(ctx, w, http.StatusCreated, newVariableResponse(req.variable, []*platform.Label{})); err != nil {
logEncodingError(h.Logger, r, err)
return
}
@ -278,7 +310,13 @@ func (h *VariableHandler) handlePatchVariable(w http.ResponseWriter, r *http.Req
return
}
err = encodeResponse(ctx, w, http.StatusOK, newVariableResponse(variable))
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: variable.ID})
if err != nil {
EncodeError(ctx, err, w)
return
}
err = encodeResponse(ctx, w, http.StatusOK, newVariableResponse(variable, labels))
if err != nil {
logEncodingError(h.Logger, r, err)
return
@ -340,7 +378,13 @@ func (h *VariableHandler) handlePutVariable(w http.ResponseWriter, r *http.Reque
return
}
err = encodeResponse(ctx, w, http.StatusOK, newVariableResponse(req.variable))
labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: req.variable.ID})
if err != nil {
EncodeError(ctx, err, w)
return
}
err = encodeResponse(ctx, w, http.StatusOK, newVariableResponse(req.variable, labels))
if err != nil {
logEncodingError(h.Logger, r, err)
return

View File

@ -3,6 +3,7 @@ package http
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
@ -23,12 +24,14 @@ func NewMockVariableBackend() *VariableBackend {
return &VariableBackend{
Logger: zap.NewNop().With(zap.String("handler", "variable")),
VariableService: mock.NewVariableService(),
LabelService: mock.NewLabelService(),
}
}
func TestVariableService_handleGetVariables(t *testing.T) {
type fields struct {
VariableService platform.VariableService
LabelService platform.LabelService
}
type args struct {
queryParams map[string][]string
@ -74,11 +77,25 @@ func TestVariableService_handleGetVariables(t *testing.T) {
}, nil
},
},
&mock.LabelService{
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
labels := []*platform.Label{
{
ID: platformtesting.MustIDBase16("fc3dc670a4be9b9a"),
Name: "label",
Properties: map[string]string{
"color": "fff000",
},
},
}
return labels, nil
},
},
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `{"variables":[{"id":"6162207574726f71","orgID":"0000000000000001","name":"variable-a","selected":["b"],"arguments":{"type":"constant","values":["a","b"]},"links":{"self":"/api/v2/variables/6162207574726f71","org": "/api/v2/orgs/0000000000000001"}},{"id":"61726920617a696f","orgID":"0000000000000001","name":"variable-b","selected":["c"],"arguments":{"type":"map","values":{"a":"b","c":"d"}},"links":{"self":"/api/v2/variables/61726920617a696f","org": "/api/v2/orgs/0000000000000001"}}],"links":{"self":"/api/v2/variables?descending=false&limit=20&offset=0"}}`,
body: `{"variables":[{"id":"6162207574726f71","orgID":"0000000000000001","name":"variable-a","selected":["b"],"arguments":{"type":"constant","values":["a","b"]},"labels":[{"id":"fc3dc670a4be9b9a","name":"label","properties":{"color":"fff000"}}],"links":{"self":"/api/v2/variables/6162207574726f71","labels":"/api/v2/variables/6162207574726f71/labels","org":"/api/v2/orgs/0000000000000001"}},{"id":"61726920617a696f","orgID":"0000000000000001","name":"variable-b","selected":["c"],"arguments":{"type":"map","values":{"a":"b","c":"d"}},"labels":[{"id":"fc3dc670a4be9b9a","name":"label","properties":{"color":"fff000"}}],"links":{"self":"/api/v2/variables/61726920617a696f","labels":"/api/v2/variables/61726920617a696f/labels","org": "/api/v2/orgs/0000000000000001"}}],"links":{"self":"/api/v2/variables?descending=false&limit=20&offset=0"}}`,
},
},
{
@ -89,6 +106,11 @@ func TestVariableService_handleGetVariables(t *testing.T) {
return []*platform.Variable{}, nil
},
},
&mock.LabelService{
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
return []*platform.Label{}, nil
},
},
},
args: args{
map[string][]string{
@ -120,6 +142,20 @@ func TestVariableService_handleGetVariables(t *testing.T) {
}, nil
},
},
&mock.LabelService{
FindResourceLabelsFn: func(ctx context.Context, f platform.LabelMappingFilter) ([]*platform.Label, error) {
labels := []*platform.Label{
{
ID: platformtesting.MustIDBase16("fc3dc670a4be9b9a"),
Name: "label",
Properties: map[string]string{
"color": "fff000",
},
},
}
return labels, nil
},
},
},
args: args{
map[string][]string{
@ -129,7 +165,7 @@ func TestVariableService_handleGetVariables(t *testing.T) {
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: `{"variables":[{"id":"6162207574726f71","orgID":"0000000000000001","name":"variable-a","selected":["b"],"arguments":{"type":"constant","values":["a","b"]},"links":{"self":"/api/v2/variables/6162207574726f71","org":"/api/v2/orgs/0000000000000001"}}],"links":{"self":"/api/v2/variables?descending=false&limit=20&offset=0&orgID=0000000000000001"}}`,
body: `{"variables":[{"id":"6162207574726f71","orgID":"0000000000000001","name":"variable-a","selected":["b"],"arguments":{"type":"constant","values":["a","b"]},"labels":[{"id":"fc3dc670a4be9b9a","name":"label","properties":{"color": "fff000"}}],"links":{"self":"/api/v2/variables/6162207574726f71","org":"/api/v2/orgs/0000000000000001","labels":"/api/v2/variables/6162207574726f71/labels"}}],"links":{"self":"/api/v2/variables?descending=false&limit=20&offset=0&orgID=0000000000000001"}}`,
},
},
}
@ -137,10 +173,12 @@ func TestVariableService_handleGetVariables(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
variableBackend := NewMockVariableBackend()
variableBackend.LabelService = tt.fields.LabelService
variableBackend.VariableService = tt.fields.VariableService
h := NewVariableHandler(variableBackend)
r := httptest.NewRequest("GET", "http://howdy.tld", nil)
qp := r.URL.Query()
for k, vs := range tt.args.queryParams {
for _, v := range vs {
@ -213,7 +251,7 @@ func TestVariableService_handleGetVariable(t *testing.T) {
wants: wants{
statusCode: 200,
contentType: "application/json; charset=utf-8",
body: `{"id":"75650d0a636f6d70","orgID":"0000000000000001","name":"variable-a","selected":["b"],"arguments":{"type":"constant","values":["a","b"]},"links":{"self":"/api/v2/variables/75650d0a636f6d70","org":"/api/v2/orgs/0000000000000001"}}
body: `{"id":"75650d0a636f6d70","orgID":"0000000000000001","name":"variable-a","selected":["b"],"arguments":{"type":"constant","values":["a","b"]},"labels":[],"links":{"self":"/api/v2/variables/75650d0a636f6d70","labels":"/api/v2/variables/75650d0a636f6d70/labels","org":"/api/v2/orgs/0000000000000001"}}
`,
},
},
@ -291,7 +329,6 @@ func TestVariableService_handleGetVariable(t *testing.T) {
if body != tt.wants.body {
t.Errorf("got = %v, want %v", body, tt.wants.body)
}
})
}
}
@ -347,7 +384,7 @@ func TestVariableService_handlePostVariable(t *testing.T) {
wants: wants{
statusCode: 201,
contentType: "application/json; charset=utf-8",
body: `{"id":"75650d0a636f6d70","orgID":"0000000000000001","name":"my-great-variable","selected":["'foo'"],"arguments":{"type":"constant","values":["bar","foo"]},"links":{"self":"/api/v2/variables/75650d0a636f6d70","org":"/api/v2/orgs/0000000000000001"}}
body: `{"id":"75650d0a636f6d70","orgID":"0000000000000001","name":"my-great-variable","selected":["'foo'"],"arguments":{"type":"constant","values":["bar","foo"]},"labels":[],"links":{"self":"/api/v2/variables/75650d0a636f6d70","labels":"/api/v2/variables/75650d0a636f6d70/labels","org":"/api/v2/orgs/0000000000000001"}}
`,
},
},
@ -464,7 +501,7 @@ func TestVariableService_handlePatchVariable(t *testing.T) {
wants: wants{
statusCode: 200,
contentType: "application/json; charset=utf-8",
body: `{"id":"75650d0a636f6d70","orgID":"0000000000000002","name":"new-name","selected":[],"arguments":{"type":"constant","values":[]},"links":{"self":"/api/v2/variables/75650d0a636f6d70","org":"/api/v2/orgs/0000000000000002"}}
body: `{"id":"75650d0a636f6d70","orgID":"0000000000000002","name":"new-name","selected":[],"arguments":{"type":"constant","values":[]},"labels":[],"links":{"self":"/api/v2/variables/75650d0a636f6d70","labels":"/api/v2/variables/75650d0a636f6d70/labels","org":"/api/v2/orgs/0000000000000002"}}
`,
},
},
@ -604,6 +641,104 @@ func TestVariableService_handleDeleteVariable(t *testing.T) {
}
}
func TestService_handlePostVariableLabel(t *testing.T) {
type fields struct {
LabelService platform.LabelService
}
type args struct {
labelMapping *platform.LabelMapping
variableID platform.ID
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "add label to variable",
fields: fields{
LabelService: &mock.LabelService{
FindLabelByIDFn: func(ctx context.Context, id platform.ID) (*platform.Label, error) {
return &platform.Label{
ID: 1,
Name: "label",
Properties: map[string]string{
"color": "fff000",
},
}, nil
},
CreateLabelMappingFn: func(ctx context.Context, m *platform.LabelMapping) error { return nil },
},
},
args: args{
labelMapping: &platform.LabelMapping{
ResourceID: 100,
LabelID: 1,
},
variableID: 100,
},
wants: wants{
statusCode: http.StatusCreated,
contentType: "application/json; charset=utf-8",
body: `
{
"label": {
"id": "0000000000000001",
"name": "label",
"properties": {
"color": "fff000"
}
},
"links": {
"self": "/api/v2/labels/0000000000000001"
}
}
`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
variableBackend := NewMockVariableBackend()
variableBackend.LabelService = tt.fields.LabelService
h := NewVariableHandler(variableBackend)
b, err := json.Marshal(tt.args.labelMapping)
if err != nil {
t.Fatalf("failed to unmarshal label mapping: %v", err)
}
url := fmt.Sprintf("http://localhost:9999/api/v2/variables/%s/labels", tt.args.variableID)
r := httptest.NewRequest("POST", url, bytes.NewReader(b))
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("got %v, want %v", res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("got %v, want %v", content, tt.wants.contentType)
}
if eq, diff, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("Diff\n%s", diff)
}
})
}
}
func initVariableService(f platformtesting.VariableFields, t *testing.T) (platform.VariableService, string, func()) {
t.Helper()
svc := inmem.NewService()