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

Reverse lookup tenant by cluster/namespace (openshiftio/openshiftio#1389) #476

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there only can be one result, why bother with a list at all?

},
}
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