Skip to content
This repository has been archived by the owner on Mar 11, 2021. It is now read-only.

Commit

Permalink
Reverse lookup tenant by cluster/namespace (openshiftio/openshiftio#1…
Browse files Browse the repository at this point in the history
…389)

Added an endpoint for `GET /api/tenants?cluster_url=x&namespace=y`
The response will be a list of tenants with a single entry, or `404`
if none was found.
The request MUST be sent using the SA token.

Fixes openshiftio/openshiftio#1389

Signed-off-by: Xavier Coulon <xcoulon@redhat.com>
  • Loading branch information
xcoulon committed Nov 23, 2017
2 parents 611824e + bdbb91e commit 839c30c
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 44 deletions.
22 changes: 12 additions & 10 deletions controller/tenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (c *TenantController) Setup(ctx *app.SetupTenantContext) error {
}

tenant := &tenant.Tenant{ID: ttoken.Subject(), Email: ttoken.Email()}
err = c.tenantService.CreateOrUpdateTenant(tenant)
err = c.tenantService.SaveTenant(tenant)
if err != nil {
log.Error(ctx, map[string]interface{}{
"err": err,
Expand Down Expand Up @@ -245,12 +245,13 @@ func (c *TenantController) Show(ctx *app.ShowTenantContext) error {
if err != nil {
return jsonapi.JSONErrorResponse(ctx, err)
}

return ctx.OK(convertTenant(tenant, namespaces))
result := &app.TenantSingle{Data: convertTenant(tenant, namespaces)}
return ctx.OK(result)
}

// Clean runs the setup action for the tenant namespaces.
func (c *TenantController) Clean(ctx *app.CleanTenantContext) error {

token := goajwt.ContextJWT(ctx)
if token == nil {
return jsonapi.JSONErrorResponse(ctx, errors.NewUnauthorizedError("Missing JWT token"))
Expand All @@ -276,6 +277,7 @@ func (c *TenantController) Clean(ctx *app.CleanTenantContext) error {
if err != nil {
return jsonapi.JSONErrorResponse(ctx, err)
}
// TODO (xcoulon): respond with `204 No Content` instead ?
return ctx.OK([]byte{})
}

Expand Down Expand Up @@ -306,7 +308,7 @@ func InitTenant(ctx context.Context, masterURL string, service tenant.Service, c
} else if statusCode == http.StatusCreated {
if openshift.GetKind(request) == openshift.ValKindProjectRequest {
name := openshift.GetName(request)
service.CreateOrUpdateNamespace(&tenant.Namespace{
service.SaveNamespace(&tenant.Namespace{
TenantID: currentTenant.ID,
Name: name,
State: "created",
Expand All @@ -321,7 +323,7 @@ func InitTenant(ctx context.Context, masterURL string, service tenant.Service, c

} else if openshift.GetKind(request) == openshift.ValKindNamespace {
name := openshift.GetName(request)
service.CreateOrUpdateNamespace(&tenant.Namespace{
service.SaveNamespace(&tenant.Namespace{
TenantID: currentTenant.ID,
Name: name,
State: "created",
Expand Down Expand Up @@ -395,8 +397,8 @@ func (t TenantToken) Email() string {
return ""
}

func convertTenant(tenant *tenant.Tenant, namespaces []*tenant.Namespace) *app.TenantSingle {
response := app.Tenant{
func convertTenant(tenant *tenant.Tenant, namespaces []*tenant.Namespace) *app.Tenant {
result := app.Tenant{
ID: &tenant.ID,
Type: "tenants",
Attributes: &app.TenantAttributes{
Expand All @@ -408,8 +410,8 @@ func convertTenant(tenant *tenant.Tenant, namespaces []*tenant.Namespace) *app.T
}
for _, ns := range namespaces {
tenantType := string(ns.Type)
response.Attributes.Namespaces = append(
response.Attributes.Namespaces,
result.Attributes.Namespaces = append(
result.Attributes.Namespaces,
&app.NamespaceAttributes{
CreatedAt: &ns.CreatedAt,
UpdatedAt: &ns.UpdatedAt,
Expand All @@ -420,5 +422,5 @@ func convertTenant(tenant *tenant.Tenant, namespaces []*tenant.Namespace) *app.T
State: &ns.State,
})
}
return &app.TenantSingle{Data: &response}
return &result
}
2 changes: 1 addition & 1 deletion controller/tenant_kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (c *TenantKubeController) KubeConnected(ctx *app.KubeConnectedTenantKubeCon
ttoken := &TenantToken{token: token}
tenant := &tenant.Tenant{ID: ttoken.Subject(), Email: ttoken.Email()}
exists := c.tenantService.Exists(ttoken.Subject())
err = c.tenantService.CreateOrUpdateTenant(tenant)
err = c.tenantService.SaveTenant(tenant)
if err == nil {
tenantID := ttoken.Subject()
tenant, err := c.tenantService.GetTenant(tenantID)
Expand Down
30 changes: 29 additions & 1 deletion controller/tenants.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,34 @@ func (c *TenantsController) Show(ctx *app.ShowTenantsContext) error {
if err != nil {
return jsonapi.JSONErrorResponse(ctx, err)
}
result := &app.TenantSingle{Data: convertTenant(tenant, namespaces)}
return ctx.OK(result)
}

// Search runs the search action.
func (c *TenantsController) Search(ctx *app.SearchTenantsContext) error {
if !keycloak.IsSpecificServiceAccount(ctx, "fabric8-jenkins-idler") {
return jsonapi.JSONErrorResponse(ctx, errors.NewUnauthorizedError("Wrong token"))
}

tenant, err := c.tenantService.LookupTenantByClusterAndNamespace(ctx.MasterURL, ctx.Namespace)
if err != nil {
return jsonapi.JSONErrorResponse(ctx, err)
}

return ctx.OK(convertTenant(tenant, namespaces))
namespaces, err := c.tenantService.GetNamespaces(tenant.ID)
if err != nil {
return jsonapi.JSONErrorResponse(ctx, err)
}

result := app.TenantList{
Data: []*app.Tenant{
convertTenant(tenant, namespaces),
},
// skipping the paging links for now
Meta: &app.TenantListMeta{
TotalCount: 1,
},
}
return ctx.OK(&result)
}
110 changes: 93 additions & 17 deletions controller/tenants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,103 @@ package controller

import (
"context"
"fmt"
"testing"
"time"

jwt "github.com/dgrijalva/jwt-go"
"github.com/fabric8-services/fabric8-tenant/app/test"
"github.com/fabric8-services/fabric8-tenant/tenant"
"github.com/fabric8-services/fabric8-tenant/test/gormsupport"
"github.com/fabric8-services/fabric8-tenant/test/testfixture"
"github.com/fabric8-services/fabric8-wit/errors"
"github.com/fabric8-services/fabric8-wit/resource"
"github.com/goadesign/goa"
goajwt "github.com/goadesign/goa/middleware/security/jwt"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

func TestTenants(t *testing.T) {
tenantID := uuid.NewV4()
svc := goa.New("Tenants-service")
ctrl := NewTenantsController(svc, ctrlTestService{ID: tenantID})
t.Run("OK", func(t *testing.T) {
type TenantControllerTestSuite struct {
gormsupport.DBTestSuite
}

func TestTenantController(t *testing.T) {
resource.Require(t, resource.Database)
suite.Run(t, &TenantControllerTestSuite{DBTestSuite: gormsupport.NewDBTestSuite("../config.yaml")})
}

func (s *TenantControllerTestSuite) TestShowTenants() {

s.T().Run("OK", func(t *testing.T) {
// given
tenantID := uuid.NewV4()
svc := goa.New("Tenants-service")
ctrl := NewTenantsController(svc, mockTenantService{ID: tenantID})
// when
_, tenant := test.ShowTenantsOK(t, createValidSAContext(), svc, ctrl, tenantID)
// then
assert.Equal(t, tenantID, *tenant.Data.ID)
assert.Equal(t, 1, len(tenant.Data.Attributes.Namespaces))
})
t.Run("Unauhorized - no token", func(t *testing.T) {
test.ShowTenantsUnauthorized(t, context.Background(), svc, ctrl, tenantID)

s.T().Run("Failures", func(t *testing.T) {

// given
tenantID := uuid.NewV4()
svc := goa.New("Tenants-service")
ctrl := NewTenantsController(svc, mockTenantService{ID: tenantID})

t.Run("Unauhorized - no token", func(t *testing.T) {
// when/then
test.ShowTenantsUnauthorized(t, context.Background(), svc, ctrl, tenantID)
})

t.Run("Unauhorized - no SA token", func(t *testing.T) {
// when/then
test.ShowTenantsUnauthorized(t, createInvalidSAContext(), svc, ctrl, tenantID)
})

t.Run("Not found", func(t *testing.T) {
// when/then
test.ShowTenantsNotFound(t, createValidSAContext(), svc, ctrl, uuid.NewV4())
})
})
t.Run("Unauhorized - no SA token", func(t *testing.T) {
test.ShowTenantsUnauthorized(t, createInvalidSAContext(), svc, ctrl, tenantID)
}

func (s *TenantControllerTestSuite) TestSearchTenants() {
// given
svc := goa.New("Tenants-service")

s.T().Run("OK", func(t *testing.T) {
// given
ctrl := NewTenantsController(svc, tenant.NewDBService(s.DB))
fxt := testfixture.NewTestFixture(t, s.DB, testfixture.Tenants(1), testfixture.Namespaces(1))
// when
_, tenant := test.SearchTenantsOK(t, createValidSAContext(), svc, ctrl, fxt.Namespaces[0].MasterURL, fxt.Namespaces[0].Name)
// then
require.Len(t, tenant.Data, 1)
assert.Equal(t, fxt.Tenants[0].ID, *tenant.Data[0].ID)
assert.Equal(t, 1, len(tenant.Data[0].Attributes.Namespaces))
})
t.Run("Not found", func(t *testing.T) {
test.ShowTenantsNotFound(t, createValidSAContext(), svc, ctrl, uuid.NewV4())

s.T().Run("Failures", func(t *testing.T) {
ctrl := NewTenantsController(svc, mockTenantService{})

t.Run("Unauhorized - no token", func(t *testing.T) {
test.SearchTenantsUnauthorized(t, context.Background(), svc, ctrl, "foo", "bar")
})
t.Run("Unauhorized - no SA token", func(t *testing.T) {
test.SearchTenantsUnauthorized(t, createInvalidSAContext(), svc, ctrl, "foo", "bar")
})
t.Run("Not found", func(t *testing.T) {
test.SearchTenantsNotFound(t, createValidSAContext(), svc, ctrl, "foo", "bar")
})
t.Run("Internal Server Error", func(t *testing.T) {
test.SearchTenantsInternalServerError(t, createValidSAContext(), svc, ctrl, "", "")
})
})
}

Expand All @@ -48,15 +115,15 @@ func createInvalidSAContext() context.Context {
return goajwt.WithJWT(context.Background(), token)
}

type ctrlTestService struct {
type mockTenantService struct {
ID uuid.UUID
}

func (s ctrlTestService) Exists(tenantID uuid.UUID) bool {
func (s mockTenantService) Exists(tenantID uuid.UUID) bool {
return s.ID == tenantID
}

func (s ctrlTestService) GetTenant(tenantID uuid.UUID) (*tenant.Tenant, error) {
func (s mockTenantService) GetTenant(tenantID uuid.UUID) (*tenant.Tenant, error) {
if s.ID != tenantID {
return nil, errors.NewNotFoundError("tenant", tenantID.String())
}
Expand All @@ -68,7 +135,7 @@ func (s ctrlTestService) GetTenant(tenantID uuid.UUID) (*tenant.Tenant, error) {
}, nil
}

func (s ctrlTestService) GetNamespaces(tenantID uuid.UUID) ([]*tenant.Namespace, error) {
func (s mockTenantService) GetNamespaces(tenantID uuid.UUID) ([]*tenant.Namespace, error) {
if s.ID != tenantID {
return nil, errors.NewNotFoundError("tenant", tenantID.String())
}
Expand All @@ -87,10 +154,19 @@ func (s ctrlTestService) GetNamespaces(tenantID uuid.UUID) ([]*tenant.Namespace,
}, nil
}

func (s ctrlTestService) CreateOrUpdateTenant(tenant *tenant.Tenant) error {
func (s mockTenantService) SaveTenant(tenant *tenant.Tenant) error {
return nil
}

func (s ctrlTestService) CreateOrUpdateNamespace(namespace *tenant.Namespace) error {
func (s mockTenantService) SaveNamespace(namespace *tenant.Namespace) error {
return nil
}

func (s mockTenantService) LookupTenantByClusterAndNamespace(masterURL, namespace string) (*tenant.Tenant, error) {
// produce InternalServerError
if masterURL == "" || namespace == "" {
return nil, fmt.Errorf("mock error")
}
return nil, errors.NewNotFoundError("tenant", "")

}
40 changes: 40 additions & 0 deletions design/tenant.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ var tenantSingle = JSONSingle(
tenant,
nil)

var tenantListMeta = a.Type("TenantListMeta", func() {
a.Attribute("totalCount", d.Integer)
a.Required("totalCount")
})

var pagingLinks = a.Type("pagingLinks", func() {
a.Attribute("prev", d.String)
a.Attribute("next", d.String)
a.Attribute("first", d.String)
a.Attribute("last", d.String)
a.Attribute("filters", d.String)
})

var tenantList = JSONList(
"tenant", "Holds a list of Tenants",
tenant,
pagingLinks,
tenantListMeta,
)

var _ = a.Resource("tenant", func() {
a.BasePath("/api/tenant")
a.Action("setup", func() {
Expand Down Expand Up @@ -136,4 +156,24 @@ var _ = a.Resource("tenants", func() {
a.Response(d.InternalServerError, JSONAPIErrors)
a.Response(d.Unauthorized, JSONAPIErrors)
})

a.Action("search", func() {
a.Security("jwt")
a.Routing(
a.GET(""),
)
a.Params(func() {
a.Param("master_url", d.String, "the URL of the OSO cluster where the user's project are located")
a.Param("namespace", d.String, "the user's namespace (ie, the name of the OSO 'base' project)")
a.Required("master_url")
a.Required("namespace")
})

a.Description("Lookup a tenant by cluster/namespace.")
a.Response(d.OK, tenantList)
a.Response(d.BadRequest, JSONAPIErrors)
a.Response(d.NotFound, JSONAPIErrors)
a.Response(d.InternalServerError, JSONAPIErrors)
a.Response(d.Unauthorized, JSONAPIErrors)
})
})
27 changes: 23 additions & 4 deletions tenant/service.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package tenant

import (
"fmt"

"github.com/fabric8-services/fabric8-wit/errors"
"github.com/jinzhu/gorm"
errs "github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
)

type Service interface {
Exists(tenantID uuid.UUID) bool
GetTenant(tenantID uuid.UUID) (*Tenant, error)
LookupTenantByClusterAndNamespace(masterURL, namespace string) (*Tenant, error)
GetNamespaces(tenantID uuid.UUID) ([]*Namespace, error)
CreateOrUpdateTenant(tenant *Tenant) error
CreateOrUpdateNamespace(namespace *Namespace) error
SaveTenant(tenant *Tenant) error
SaveNamespace(namespace *Namespace) error
}

func NewDBService(db *gorm.DB) Service {
Expand Down Expand Up @@ -39,14 +44,28 @@ func (s DBService) GetTenant(tenantID uuid.UUID) (*Tenant, error) {
return &t, nil
}

func (s DBService) CreateOrUpdateTenant(tenant *Tenant) error {
func (s DBService) LookupTenantByClusterAndNamespace(masterURL, namespace string) (*Tenant, error) {
// select t.id from tenant t, namespaces n where t.id = n.tenant_id and n.master_url = ? and n.name = ?
query := fmt.Sprintf("select t.* from %[1]s t, %[2]s n where t.id = n.tenant_id and n.master_url = ? and n.name = ?", Tenant{}.TableName(), Namespace{}.TableName())
var result Tenant
err := s.db.Raw(query, masterURL, namespace).Scan(&result).Error
if err == gorm.ErrRecordNotFound {
// no match
return nil, errors.NewNotFoundError("tenant", "")
} else if err != nil {
return nil, errs.Wrapf(err, "unable to lookup tenant by namespace")
}
return &result, nil
}

func (s DBService) SaveTenant(tenant *Tenant) error {
if tenant.Profile == "" {
tenant.Profile = "free"
}
return s.db.Save(tenant).Error
}

func (s DBService) CreateOrUpdateNamespace(namespace *Namespace) error {
func (s DBService) SaveNamespace(namespace *Namespace) error {
if namespace.ID == uuid.Nil {
namespace.ID = uuid.NewV4()
}
Expand Down
Loading

0 comments on commit 839c30c

Please sign in to comment.