diff --git a/http/swagger.yml b/http/swagger.yml index d0472f7fc9..755867e4de 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -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: diff --git a/http/variable_service.go b/http/variable_service.go index 81bb25e025..22a6458fe5 100644 --- a/http/variable_service.go +++ b/http/variable_service.go @@ -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 diff --git a/http/variable_test.go b/http/variable_test.go index f3de3223e1..a86ee32527 100644 --- a/http/variable_test.go +++ b/http/variable_test.go @@ -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()