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

Commit

Permalink
Refactor tenant service code (#530)
Browse files Browse the repository at this point in the history
* Renaming some `Client` into `Service`
* Moving generate auth client code in `auth/client`
* Adapted some tests (using `require.Error(t, err)` and `require.NoError(t, err)` when applicable)
* Rename 'TokenResolver' and 'TenantResolver'
* Renaming to `ResolveToken` and `ResolveTenant` respectively, so their name is a verb, since these are functions.
* More 'TenantResolver' -> 'ResolveTenant' vars renaming
* Rename func 'ClusterResolver' to 'ResolveCluster'
* Dispatch user, cluster and token in their own packages and simplify names
* Moving OpenShift Config init in openshift pkg and removing some unnecessary pointers
* Move some code from 'keycloak' pkg to 'tenant'
* Move public keys into token, fetch using auth client. Remove pointers

Signed-off-by: Xavier Coulon <[email protected]>

Fixes openshiftio/openshift.io#2266
  • Loading branch information
xcoulon authored and aslakknutsen committed Feb 21, 2018
1 parent c25ee74 commit 5419db7
Show file tree
Hide file tree
Showing 49 changed files with 1,336 additions and 1,142 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ swagger/
tool/cli/
migration/sqlbindata.go
template/bindata.go
auth
auth/client

# Ignore artifacts directory
bin/
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ $(GO_JUNIT_BIN): $(VENDOR_DIR)

CLEAN_TARGETS += clean-artifacts
.PHONY: clean-artifacts



## Removes the ./bin directory.
clean-artifacts:
-rm -rf $(INSTALL_PREFIX)
Expand All @@ -185,7 +188,7 @@ clean-generated:
-rm -rf ./swagger/
-rm -f ./migration/sqlbindata.go
-rm -f ./template/bindata.go
-rm -rf ./auth
-rm -rf ./auth/client

CLEAN_TARGETS += clean-vendor
.PHONY: clean-vendor
Expand All @@ -211,7 +214,7 @@ app/controllers.go: $(DESIGNS) $(GOAGEN_BIN) $(VENDOR_DIR)
$(GOAGEN_BIN) app -d ${PACKAGE_NAME}/${DESIGN_DIR}
$(GOAGEN_BIN) controller -d ${PACKAGE_NAME}/${DESIGN_DIR} -o controller/ --pkg controller --app-pkg app
$(GOAGEN_BIN) swagger -d ${PACKAGE_NAME}/${DESIGN_DIR}
$(GOAGEN_BIN) client -d github.com/fabric8-services/fabric8-auth/design --notool --pkg auth
$(GOAGEN_BIN) client -d github.com/fabric8-services/fabric8-auth/design --notool --out auth --pkg client


.PHONY: migrate-database
Expand Down
97 changes: 97 additions & 0 deletions auth/auth_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package auth

import (
"context"
"fmt"
"net/http"
"net/url"

authclient "github.com/fabric8-services/fabric8-tenant/auth/client"
goaclient "github.com/goadesign/goa/client"
)

type clientImpl struct {
client authclient.Client
}

// NewClient returns a new auth client
func NewClient(authURL string, options ...ClientOption) (*authclient.Client, error) {
u, err := url.Parse(authURL)
if err != nil {
return nil, err
}
c := doerConfig{
httpClient: http.DefaultClient,
}
// apply options
for _, opt := range options {
opt(&c)
}
client := authclient.New(newDoer(c))
client.Host = u.Host
client.Scheme = u.Scheme
return client, nil
}

// ClientOption a function to customize the auth client
type ClientOption func(*doerConfig)

type doerConfig struct {
httpClient *http.Client
token *string
}

// WithHTTPClient an option to specify the http client to use
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(c *doerConfig) {
c.httpClient = httpClient
}
}

// WithToken an option to specify the token to use
func WithToken(token string) ClientOption {
return func(c *doerConfig) {
c.token = &token
}
}

type doer struct {
target goaclient.Doer
token *string
}

// Doer adds Authorization to all Requests, un related to goa design
func newDoer(config doerConfig) goaclient.Doer {
return &doer{
target: goaclient.HTTPClientDoer(config.httpClient),
token: config.token,
}
}

func (d *doer) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
if d.token != nil {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *d.token))
}
return d.target.Do(ctx, req)
}

// ValidateError function when given client and response checks if the
// response has any errors by also looking at the status code
func ValidateError(c *authclient.Client, res *http.Response) error {
if res.StatusCode == http.StatusNotFound {
return fmt.Errorf("404 Not found")
} else if res.StatusCode != http.StatusOK {
goaErr, err := c.DecodeJSONAPIErrors(res)
if err != nil {
return err
}
if len(goaErr.Errors) != 0 {
var output string
for _, error := range goaErr.Errors {
output += fmt.Sprintf("%s: %s %s, %s\n", *error.Title, *error.Status, *error.Code, error.Detail)
}
return fmt.Errorf("%s", output)
}
}
return nil
}
21 changes: 21 additions & 0 deletions cluster/resolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cluster

import (
"context"
"fmt"
)

// Resolve a func to resolve a cluster
type Resolve func(ctx context.Context, target string) (*Cluster, error)

// NewResolve returns a new Cluster
func NewResolve(clusters []*Cluster) Resolve {
return func(ctx context.Context, target string) (*Cluster, error) {
for _, cluster := range clusters {
if cleanURL(target) == cleanURL(cluster.APIURL) {
return cluster, nil
}
}
return nil, fmt.Errorf("unable to resolve cluster")
}
}
98 changes: 98 additions & 0 deletions cluster/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cluster

import (
"context"
"io/ioutil"
"strings"

"github.com/fabric8-services/fabric8-tenant/auth"
authclient "github.com/fabric8-services/fabric8-tenant/auth/client"
"github.com/fabric8-services/fabric8-tenant/token"
goaclient "github.com/goadesign/goa/client"
"github.com/pkg/errors"
)

// Cluster a cluster
type Cluster struct {
APIURL string
ConsoleURL string
MetricsURL string
AppDNS string

User string
Token string
}

func cleanURL(url string) string {
if !strings.HasSuffix(url, "/") {
return url + "/"
}
return url
}

// Service the interface for the cluster service
type Service interface {
GetClusters(context.Context) ([]*Cluster, error)
}

// NewService creates a Resolver that rely on the Auth service to retrieve tokens
func NewService(authURL string, serviceToken string, resolveToken token.Resolve, decode token.Decode, options ...auth.ClientOption) Service {
return &clusterService{authURL: authURL, serviceToken: serviceToken, resolveToken: resolveToken, decode: decode, clientOptions: options}
}

type clusterService struct {
authURL string
clientOptions []auth.ClientOption
serviceToken string
resolveToken token.Resolve
decode token.Decode
}

func (s *clusterService) GetClusters(ctx context.Context) ([]*Cluster, error) {
client, err := auth.NewClient(s.authURL, s.clientOptions...)
if err != nil {
return nil, err
}
client.SetJWTSigner(
&goaclient.JWTSigner{
TokenSource: &goaclient.StaticTokenSource{
StaticToken: &goaclient.StaticToken{
Value: s.serviceToken,
Type: "Bearer"}}})

res, err := client.ShowClusters(ctx, authclient.ShowClustersPath())
if err != nil {
return nil, errors.Wrapf(err, "error while doing the request")
}
defer func() {
ioutil.ReadAll(res.Body)
res.Body.Close()
}()

validationerror := auth.ValidateError(client, res)
if validationerror != nil {
return nil, errors.Wrapf(validationerror, "error from server %q", s.authURL)
}

clusters, err := client.DecodeClusterList(res)
if err != nil {
return nil, errors.Wrapf(err, "error from server %q", s.authURL)
}

var cls []*Cluster
for _, cluster := range clusters.Data {
clusterUser, clusterToken, err := s.resolveToken(ctx, cluster.APIURL, s.serviceToken, s.decode)
if err != nil {
return nil, errors.Wrapf(err, "Unable to resolve token for cluster %v", cluster.APIURL)
}
cls = append(cls, &Cluster{
APIURL: cluster.APIURL,
AppDNS: cluster.AppDNS,
ConsoleURL: cluster.ConsoleURL,
MetricsURL: cluster.MetricsURL,
User: clusterUser,
Token: clusterToken,
})
}
return cls, nil
}
75 changes: 35 additions & 40 deletions token/clusters_test.go → cluster/service_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package token_test
package cluster_test

import (
"context"
Expand All @@ -7,72 +7,72 @@ import (
"os"
"testing"

"github.com/fabric8-services/fabric8-tenant/cluster"
"github.com/fabric8-services/fabric8-tenant/configuration"
"github.com/fabric8-services/fabric8-tenant/token"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestClusterCache(t *testing.T) {

t.Run("cluster - end slash", func(t *testing.T) {
// given
target := "A"

c := token.NewCachedClusterResolver([]*token.Cluster{
resolve := cluster.NewResolve([]*cluster.Cluster{
{APIURL: "X"},
{APIURL: target + "/"},
})

found, err := c(context.Background(), target)
if err != nil {
t.Error(err)
}
// when
found, err := resolve(context.Background(), target)
// then
require.NoError(t, err)
assert.Contains(t, found.APIURL, target)
})
t.Run("cluster - no end slash", func(t *testing.T) {
// given
target := "A"

c := token.NewCachedClusterResolver([]*token.Cluster{
resolve := cluster.NewResolve([]*cluster.Cluster{
{APIURL: "X"},
{APIURL: target},
})

found, err := c(context.Background(), target+"/")
if err != nil {
t.Error(err)
}
// when
found, err := resolve(context.Background(), target+"/")
// then
require.NoError(t, err)
assert.Contains(t, found.APIURL, target)
})

t.Run("both slash", func(t *testing.T) {
// given
target := "A"

c := token.NewCachedClusterResolver([]*token.Cluster{
resolve := cluster.NewResolve([]*cluster.Cluster{
{APIURL: "X"},
{APIURL: target + "/"},
})

found, err := c(context.Background(), target+"/")
if err != nil {
t.Error(err)
}
// when
found, err := resolve(context.Background(), target+"/")
// then
require.NoError(t, err)
assert.Contains(t, found.APIURL, target)
})

t.Run("no slash", func(t *testing.T) {
// given
target := "A"

c := token.NewCachedClusterResolver([]*token.Cluster{
resolve := cluster.NewResolve([]*cluster.Cluster{
{APIURL: "X"},
{APIURL: target + "/"},
})

found, err := c(context.Background(), target+"/")
if err != nil {
t.Error(err)
}
// when
found, err := resolve(context.Background(), target+"/")
// then
require.NoError(t, err)
assert.Contains(t, found.APIURL, target)
})
}

func TestClusterResolver(t *testing.T) {
func TestResolveCluster(t *testing.T) {
tests := []struct {
name string
status int
Expand Down Expand Up @@ -105,17 +105,12 @@ func TestClusterResolver(t *testing.T) {
t.Fatal(err)
}
tr := func(ctx context.Context, target, token string, decode token.Decode) (user, accessToken string, err error) {
return "", "", nil
}

cresolver := token.NewAuthClusterClient(config, "aa", tr, token.PlainTextToken)
clusters, err := cresolver.Get(context.Background())
if err != nil {
t.Fatal(err)
}
if len(clusters) != tt.count {
t.Errorf("Wrong number of clusters, got %v but expected %v", len(clusters), tt.count)
return "foo", "bar", nil
}
cs := cluster.NewService(config.GetAuthURL(), "aa", tr, token.PlainText)
clusters, err := cs.GetClusters(context.Background())
require.NoError(t, err)
assert.Len(t, clusters, tt.count)
})
}
}
Loading

0 comments on commit 5419db7

Please sign in to comment.