Skip to content

Commit

Permalink
tag validator service (#2670)
Browse files Browse the repository at this point in the history
* Add first draft to tag validation server

* initialize noop tag service

* Fix lint

* Add support to call tag service via GRPC

* Add tests for validation of apps and services
  • Loading branch information
wpjunior committed Dec 12, 2023
1 parent 31e75c9 commit 1cbc08a
Show file tree
Hide file tree
Showing 14 changed files with 705 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,11 @@ SWAGGER=$(GOBIN)/swagger
else
SWAGGER=$(shell command -v swagger)
endif


PROTOC ?= protoc
.PHONY: generate
generate-grpc:
$(PROTOC) --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
types/tag/service.proto
28 changes: 28 additions & 0 deletions api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
logTypes "github.com/tsuru/tsuru/types/log"
permTypes "github.com/tsuru/tsuru/types/permission"
"github.com/tsuru/tsuru/types/quota"
tagTypes "github.com/tsuru/tsuru/types/tag"
)

var (
Expand Down Expand Up @@ -453,6 +454,18 @@ func createApp(w http.ResponseWriter, r *http.Request, t auth.Token) (err error)
}
}
}

tagResponse, err := servicemanager.Tag.Validate(ctx, &tagTypes.TagValidationRequest{
Operation: tagTypes.OperationKind_OPERATION_KIND_CREATE,
Tags: a.Tags,
})
if err != nil {
return err
}
if !tagResponse.Valid {
return &errors.HTTP{Code: http.StatusBadRequest, Message: tagResponse.Error}
}

evt, err := event.New(&event.Opts{
Target: appTarget(a.Name),
Kind: permission.PermAppCreate,
Expand Down Expand Up @@ -612,6 +625,21 @@ func updateApp(w http.ResponseWriter, r *http.Request, t auth.Token) (err error)
return permission.ErrUnauthorized
}
}

if len(updateData.Tags) > 0 {
var tagResponse *tagTypes.ValidationResponse
tagResponse, err = servicemanager.Tag.Validate(ctx, &tagTypes.TagValidationRequest{
Operation: tagTypes.OperationKind_OPERATION_KIND_UPDATE,
Tags: updateData.Tags,
})
if err != nil {
return err
}
if !tagResponse.Valid {
return &errors.HTTP{Code: http.StatusBadRequest, Message: tagResponse.Error}
}
}

evt, err := event.New(&event.Opts{
Target: appTarget(appName),
Kind: permission.PermAppUpdate,
Expand Down
65 changes: 65 additions & 0 deletions api/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
logTypes "github.com/tsuru/tsuru/types/log"
permTypes "github.com/tsuru/tsuru/types/permission"
"github.com/tsuru/tsuru/types/quota"
tagTypes "github.com/tsuru/tsuru/types/tag"
check "gopkg.in/check.v1"
)

Expand Down Expand Up @@ -1296,6 +1297,37 @@ func (s *S) TestCreateAppWithTags(c *check.C) {
}, eventtest.HasEvent)
}

func (s *S) TestCreateAppWithTagsAndTagValidator(c *check.C) {
previousTagService := servicemanager.Tag
defer func() {
servicemanager.Tag = previousTagService
}()
servicemanager.Tag = &tagTypes.MockServiceTagServiceClient{
OnValidate: func(in *tagTypes.TagValidationRequest) (*tagTypes.ValidationResponse, error) {
c.Assert(in.Operation, check.Equals, tagTypes.OperationKind_OPERATION_KIND_CREATE)
c.Assert(in.Tags, check.DeepEquals, []string{"tag0", "tag1", "tag2"})
return &tagTypes.ValidationResponse{Valid: false, Error: "invalid tag"}, nil
},
}

s.setupMockForCreateApp(c, "zend")
data, err := url.QueryUnescape("name=someapp&platform=zend&tag=tag1&tag=tag2&tags.0=tag0")
c.Assert(err, check.IsNil)
b := strings.NewReader(data)
request, err := http.NewRequest("POST", "/apps", b)
c.Assert(err, check.IsNil)
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
recorder := httptest.NewRecorder()
token := userWithPermission(c, permission.Permission{
Scheme: permission.PermAppCreate,
Context: permission.Context(permTypes.CtxTeam, s.team.Name),
})
request.Header.Set("Authorization", "b "+token.GetValue())
s.testServer.ServeHTTP(recorder, request)
c.Assert(recorder.Code, check.Equals, http.StatusBadRequest)
c.Assert(recorder.Body.String(), check.Equals, "invalid tag\n")
}

func (s *S) TestCreateAppWithMetadata(c *check.C) {
s.setupMockForCreateApp(c, "zend")
data, err := url.QueryUnescape("name=someapp&platform=zend&metadata.annotations.0.name=a&metadata.annotations.0.value=b")
Expand Down Expand Up @@ -1798,6 +1830,39 @@ func (s *S) TestUpdateAppWithTagsOnly(c *check.C) {
}, eventtest.HasEvent)
}

func (s *S) TestUpdateAppWithTagsAndTagValidator(c *check.C) {
previousTagService := servicemanager.Tag
defer func() {
servicemanager.Tag = previousTagService
}()
servicemanager.Tag = &tagTypes.MockServiceTagServiceClient{
OnValidate: func(in *tagTypes.TagValidationRequest) (*tagTypes.ValidationResponse, error) {
c.Assert(in.Operation, check.Equals, tagTypes.OperationKind_OPERATION_KIND_UPDATE)
c.Assert(in.Tags, check.DeepEquals, []string{"tag0", "tag1", "tag2", "tag3"})
return &tagTypes.ValidationResponse{Valid: false, Error: "invalid tag"}, nil
},
}
a := app.App{Name: "myapp", Platform: "zend", TeamOwner: s.team.Name}
err := app.CreateApp(context.TODO(), &a, s.user)
c.Assert(err, check.IsNil)

token := userWithPermission(c, permission.Permission{
Scheme: permission.PermAppUpdate,
Context: permission.Context(permTypes.CtxApp, a.Name),
})
b := strings.NewReader("description1=s&tag=tag1&tag=tag2&tag=tag3&tags.0=tag0")
request, err := http.NewRequest("PUT", "/apps/myapp", b)
c.Assert(err, check.IsNil)

request.Header.Set("Authorization", "bearer "+token.GetValue())
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

recorder := httptest.NewRecorder()
s.testServer.ServeHTTP(recorder, request)
c.Assert(recorder.Code, check.Equals, http.StatusBadRequest)
c.Assert(recorder.Body.String(), check.Equals, "invalid tag\n")
}

func (s *S) TestUpdateAppWithTagsWithoutPermission(c *check.C) {
a := app.App{Name: "myapp", Platform: "zend", TeamOwner: s.team.Name}
err := app.CreateApp(context.TODO(), &a, s.user)
Expand Down
5 changes: 5 additions & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
"github.com/tsuru/tsuru/service"
"github.com/tsuru/tsuru/servicemanager"
"github.com/tsuru/tsuru/storage"
"github.com/tsuru/tsuru/tag"
appTypes "github.com/tsuru/tsuru/types/app"
"github.com/tsuru/tsuru/volume"
"golang.org/x/net/websocket"
Expand Down Expand Up @@ -191,6 +192,10 @@ func setupServices() error {
if err != nil {
return errors.Wrapf(err, "could not initialize job service")
}
servicemanager.Tag, err = tag.TagService()
if err != nil {
return errors.Wrapf(err, "could not initialize tag service")
}
return nil
}

Expand Down
23 changes: 23 additions & 0 deletions api/service_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import (
"github.com/tsuru/tsuru/permission"
"github.com/tsuru/tsuru/provision/pool"
"github.com/tsuru/tsuru/service"
"github.com/tsuru/tsuru/servicemanager"
permTypes "github.com/tsuru/tsuru/types/permission"
tagTypes "github.com/tsuru/tsuru/types/tag"
)

func serviceInstanceTarget(name, instance string) event.Target {
Expand Down Expand Up @@ -88,6 +90,17 @@ func createServiceInstance(w http.ResponseWriter, r *http.Request, t auth.Token)
}
}

tagResponse, err := servicemanager.Tag.Validate(ctx, &tagTypes.TagValidationRequest{
Operation: tagTypes.OperationKind_OPERATION_KIND_CREATE,
Tags: instance.Tags,
})
if err != nil {
return err
}
if !tagResponse.Valid {
return &tsuruErrors.HTTP{Code: http.StatusBadRequest, Message: tagResponse.Error}
}

evt, err := event.New(&event.Opts{
Target: serviceInstanceTarget(serviceName, instance.Name),
Kind: permission.PermServiceInstanceCreate,
Expand Down Expand Up @@ -197,6 +210,16 @@ func updateServiceInstance(w http.ResponseWriter, r *http.Request, t auth.Token)
return permission.ErrUnauthorized
}
}
tagResponse, err := servicemanager.Tag.Validate(ctx, &tagTypes.TagValidationRequest{
Operation: tagTypes.OperationKind_OPERATION_KIND_UPDATE,
Tags: si.Tags,
})
if err != nil {
return err
}
if !tagResponse.Valid {
return &tsuruErrors.HTTP{Code: http.StatusBadRequest, Message: tagResponse.Error}
}
evt, err := event.New(&event.Opts{
Target: serviceInstanceTarget(serviceName, instanceName),
Kind: permission.PermServiceInstanceUpdate,
Expand Down
71 changes: 71 additions & 0 deletions api/service_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/tsuru/tsuru/provision/provisiontest"
"github.com/tsuru/tsuru/router/routertest"
"github.com/tsuru/tsuru/service"
"github.com/tsuru/tsuru/servicemanager"
servicemock "github.com/tsuru/tsuru/servicemanager/mock"
_ "github.com/tsuru/tsuru/storage/mongodb"
tsuruTest "github.com/tsuru/tsuru/test"
Expand All @@ -42,6 +43,7 @@ import (
permTypes "github.com/tsuru/tsuru/types/permission"
provisionTypes "github.com/tsuru/tsuru/types/provision"
serviceTypes "github.com/tsuru/tsuru/types/service"
tagTypes "github.com/tsuru/tsuru/types/tag"
"golang.org/x/crypto/bcrypt"
check "gopkg.in/check.v1"
)
Expand Down Expand Up @@ -559,6 +561,36 @@ func (s *ServiceInstanceSuite) TestCreateServiceInstanceWithTags(c *check.C) {
})
}

func (s *ServiceInstanceSuite) TestCreateServiceInstanceWithTagsAndTagValidator(c *check.C) {
previousTagService := servicemanager.Tag
defer func() {
servicemanager.Tag = previousTagService
}()
servicemanager.Tag = &tagTypes.MockServiceTagServiceClient{
OnValidate: func(in *tagTypes.TagValidationRequest) (*tagTypes.ValidationResponse, error) {
c.Assert(in.Operation, check.Equals, tagTypes.OperationKind_OPERATION_KIND_CREATE)
c.Assert(in.Tags, check.DeepEquals, []string{"tag a", "tag b"})
return &tagTypes.ValidationResponse{Valid: false, Error: "invalid tag"}, nil
},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"DATABASE_HOST":"localhost"}`))
}))
defer ts.Close()
params := map[string]interface{}{
"name": "brainsql",
"service_name": "mysql",
"plan": "small",
"owner": s.team.Name,
"tag": []string{"tag a", "tag b"},
}
recorder, request := makeRequestToCreateServiceInstance(params, c)
request.Header.Set("Authorization", "b "+s.token.GetValue())
s.testServer.ServeHTTP(recorder, request)
c.Assert(recorder.Code, check.Equals, http.StatusBadRequest)
c.Assert(recorder.Body.String(), check.Equals, "invalid tag\n")
}

func makeRequestToUpdateServiceInstance(params map[string]interface{}, serviceName, instanceName, token string, c *check.C) (*httptest.ResponseRecorder, *http.Request) {
var body bytes.Buffer
err := json.NewEncoder(&body).Encode(params)
Expand Down Expand Up @@ -714,6 +746,45 @@ func (s *ServiceInstanceSuite) TestUpdateServiceInstanceWithTags(c *check.C) {
}, eventtest.HasEvent)
}

func (s *ServiceInstanceSuite) TestUpdateServiceInstanceWithTagsAndTagValidator(c *check.C) {
previousTagService := servicemanager.Tag
defer func() {
servicemanager.Tag = previousTagService
}()
servicemanager.Tag = &tagTypes.MockServiceTagServiceClient{
OnValidate: func(in *tagTypes.TagValidationRequest) (*tagTypes.ValidationResponse, error) {
c.Assert(in.Operation, check.Equals, tagTypes.OperationKind_OPERATION_KIND_UPDATE)
c.Assert(in.Tags, check.DeepEquals, []string{"tag b", "tag c"})
return &tagTypes.ValidationResponse{Valid: false, Error: "invalid tag"}, nil
},
}
si := service.ServiceInstance{
Name: "brainsql",
ServiceName: "mysql",
Apps: []string{"other"},
Teams: []string{s.team.Name},
Tags: []string{"tag a"},
TeamOwner: s.team.Name,
}
err := s.conn.ServiceInstances().Insert(si)
c.Assert(err, check.IsNil)
params := map[string]interface{}{
"description": "",
"plan": "",
"teamowner": s.team.Name,
"tag": []string{"tag b", "tag c"},
"parameters": map[string]interface{}{},
}
_, token := permissiontest.CustomUserWithPermission(c, nativeScheme, "myuser", permission.Permission{
Scheme: permission.PermServiceInstanceUpdateTags,
Context: permission.Context(permTypes.CtxServiceInstance, serviceIntancePermName("mysql", si.Name)),
})
recorder, request := makeRequestToUpdateServiceInstance(params, "mysql", "brainsql", token.GetValue(), c)
s.testServer.ServeHTTP(recorder, request)
c.Assert(recorder.Code, check.Equals, http.StatusBadRequest)
c.Assert(recorder.Body.String(), check.Equals, "invalid tag\n")
}

func (s *ServiceInstanceSuite) TestUpdateServiceInstanceWithEmptyTagRemovesTags(c *check.C) {
si := service.ServiceInstance{
Name: "brainsql",
Expand Down
3 changes: 3 additions & 0 deletions api/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/tsuru/tsuru/servicemanager"
servicemock "github.com/tsuru/tsuru/servicemanager/mock"
_ "github.com/tsuru/tsuru/storage/mongodb"
"github.com/tsuru/tsuru/tag"
appTypes "github.com/tsuru/tsuru/types/app"
authTypes "github.com/tsuru/tsuru/types/auth"
permTypes "github.com/tsuru/tsuru/types/permission"
Expand Down Expand Up @@ -140,6 +141,8 @@ func (s *S) SetUpTest(c *check.C) {
c.Assert(err, check.IsNil)
servicemanager.Job, err = job.JobService()
c.Assert(err, check.IsNil)
servicemanager.Tag, err = tag.TagService()
c.Assert(err, check.IsNil)
}

func (s *S) setupMocks() {
Expand Down
2 changes: 2 additions & 0 deletions servicemanager/servicemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/tsuru/tsuru/types/quota"
"github.com/tsuru/tsuru/types/router"
"github.com/tsuru/tsuru/types/service"
"github.com/tsuru/tsuru/types/tag"
"github.com/tsuru/tsuru/types/tracker"
"github.com/tsuru/tsuru/types/volume"
)
Expand Down Expand Up @@ -42,4 +43,5 @@ var (
AuthGroup auth.GroupService
Pool provision.PoolService
Volume volume.VolumeService
Tag tag.TagServiceClient
)
20 changes: 20 additions & 0 deletions tag/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2023 tsuru authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package tag

import (
"context"

tagTypes "github.com/tsuru/tsuru/types/tag"
"google.golang.org/grpc"
)

var _ tagTypes.TagServiceClient = &noopTagClient{}

type noopTagClient struct{}

func (*noopTagClient) Validate(ctx context.Context, in *tagTypes.TagValidationRequest, opts ...grpc.CallOption) (*tagTypes.ValidationResponse, error) {
return &tagTypes.ValidationResponse{Valid: true}, nil
}
25 changes: 25 additions & 0 deletions tag/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2023 tsuru authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package tag

import (
"github.com/tsuru/config"
tagTypes "github.com/tsuru/tsuru/types/tag"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

func TagService() (tagTypes.TagServiceClient, error) {
tagServiceAddr, _ := config.GetString("tag:service-addr")
if tagServiceAddr != "" {
conn, err := grpc.Dial(tagServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}

return tagTypes.NewTagServiceClient(conn), nil
}
return &noopTagClient{}, nil
}

0 comments on commit 1cbc08a

Please sign in to comment.