Skip to content

Commit

Permalink
feat: add support for clusters in multiple regions
Browse files Browse the repository at this point in the history
Adding `ClientProvider` allows clusters to use different `OCIClients`
groups to interact with the regional APIs.
  • Loading branch information
joekr committed Mar 28, 2022
1 parent 76da8d3 commit 70e961b
Show file tree
Hide file tree
Showing 17 changed files with 473 additions and 114 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ bin

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
cover.html
testbin/
out/

Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ ENVTEST_ASSETS_DIR=$(shell pwd)/testbin
test: manifests generate fmt vet ## Run tests.
mkdir -p ${ENVTEST_ASSETS_DIR}
test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.8.3/hack/setup-envtest.sh
source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out
source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out; go tool cover -html=cover.out -o cover.html


##@ Build

Expand Down
4 changes: 4 additions & 0 deletions api/v1beta1/ocicluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ type OCIClusterSpec struct {
// Compartment to create the cluster network.
CompartmentId string `mandatory:"true" json:"compartmentId"`

// The OCI Region the cluster lives in. It must be one of available regions in Region Identifier format.
// See https://docs.oracle.com/en-us/iaas/Content/General/Concepts/regions.htm
Region string `json:"region,omitempty"`

// ControlPlaneEndpoint represents the endpoint used to communicate with the control plane.
// +optional
ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
Expand Down
154 changes: 154 additions & 0 deletions cloud/scope/clients.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package scope

import (
"fmt"
"sync"

"github.com/go-logr/logr"
"github.com/oracle/cluster-api-provider-oci/cloud/config"
"github.com/oracle/cluster-api-provider-oci/cloud/services/compute"
"github.com/oracle/cluster-api-provider-oci/cloud/services/vcn"
"github.com/oracle/oci-go-sdk/v63/common"
"github.com/oracle/oci-go-sdk/v63/core"
"github.com/oracle/oci-go-sdk/v63/identity"
"github.com/oracle/oci-go-sdk/v63/networkloadbalancer"
"k8s.io/klog/v2/klogr"
)

// OCIClients is the struct of all the needed OCI clients
type OCIClients struct {
VCNClient vcn.Client
LoadBalancerClient networkloadbalancer.NetworkLoadBalancerClient
IdentityClient identity.IdentityClient
ComputeClient compute.ComputeClient
}

// ClientProvider defines the regional clients
type ClientProvider struct {
*logr.Logger
ociClients map[string]OCIClients
ociClientsLock *sync.RWMutex
ociAuthConfigProvider common.ConfigurationProvider
authConfig *config.AuthConfig
}

// NewClientProvider builds the ClientProvider with a client for the given region
func NewClientProvider(region string, authConfig *config.AuthConfig) (ClientProvider, error) {
if len(region) <= 0 {
return ClientProvider{}, fmt.Errorf("NewClientProvider region can not be empty")
}

log := klogr.New()
authConfig.Region = region

ociAuthConfigProvider, err := config.NewConfigurationProvider(authConfig)
if err != nil {
log.Error(err, "authentication provider could not be initialised")
return ClientProvider{}, err
}

provider := ClientProvider{
Logger: &log,
ociAuthConfigProvider: ociAuthConfigProvider,
ociClients: map[string]OCIClients{},
ociClientsLock: new(sync.RWMutex),
authConfig: authConfig,
}
_, err = provider.GetOrBuildClient(region)
if err != nil {
return ClientProvider{}, err
}

return provider, nil
}

// GetOrBuildClient if the OCIClients exist for the region they are returned, if not clients will build them
func (c *ClientProvider) GetOrBuildClient(region string) (OCIClients, error) {
if len(region) <= 0 {
return OCIClients{}, fmt.Errorf("ClientProvider.GetOrBuildClient region can not be empty")
}

c.ociClientsLock.RLock()
clients, regionalClientsExists := c.ociClients[region]
c.ociClientsLock.RUnlock()

if regionalClientsExists {
return clients, nil
}

regionalAuth := c.authConfig
regionalAuth.Region = region
ociAuthConfigProvider, err := config.NewConfigurationProvider(regionalAuth)
if err != nil {
return OCIClients{}, err
}

c.ociClientsLock.Lock()
defer c.ociClientsLock.Unlock()
regionalClient, err := createClients(ociAuthConfigProvider, c.Logger)
if err != nil {
c.Logger.Error(err, "Error creating regional clients")
return regionalClient, err
}
c.ociClients[region] = regionalClient

return regionalClient, nil
}

func createClients(oCIAuthConfigProvider common.ConfigurationProvider, logger *logr.Logger) (OCIClients, error) {
vcnClient, err := createVncClient(oCIAuthConfigProvider, logger)
lbClient, err := createLbClient(oCIAuthConfigProvider, logger)
identityClient, err := createIdentityClient(oCIAuthConfigProvider, logger)
computeClient, err := createComputeClient(oCIAuthConfigProvider, logger)

if err != nil {
return OCIClients{}, err
}

return OCIClients{
VCNClient: vcnClient,
LoadBalancerClient: lbClient,
IdentityClient: identityClient,
ComputeClient: computeClient,
}, err
}

func createVncClient(ociAuthConfigProvider common.ConfigurationProvider, logger *logr.Logger) (core.VirtualNetworkClient, error) {
vcnClient, err := core.NewVirtualNetworkClientWithConfigurationProvider(ociAuthConfigProvider)
if err != nil {
logger.Error(err, "unable to create OCI VCN Client")
return core.VirtualNetworkClient{}, err
}

return vcnClient, nil
}

func createLbClient(ociAuthConfigProvider common.ConfigurationProvider, logger *logr.Logger) (networkloadbalancer.NetworkLoadBalancerClient, error) {
lbClient, err := networkloadbalancer.NewNetworkLoadBalancerClientWithConfigurationProvider(ociAuthConfigProvider)
if err != nil {
logger.Error(err, "unable to create OCI LB Client")
return networkloadbalancer.NetworkLoadBalancerClient{}, err
}

return lbClient, nil
}

func createIdentityClient(ociAuthConfigProvider common.ConfigurationProvider, logger *logr.Logger) (identity.IdentityClient, error) {
identityClient, err := identity.NewIdentityClientWithConfigurationProvider(ociAuthConfigProvider)
if err != nil {
logger.Error(err, "unable to create OCI Identity Client")
return identity.IdentityClient{}, err
}

return identityClient, nil
}

func createComputeClient(ociAuthConfigProvider common.ConfigurationProvider, logger *logr.Logger) (core.ComputeClient, error) {
computeClient, err := core.NewComputeClientWithConfigurationProvider(ociAuthConfigProvider)
if err != nil {
logger.Error(err, "unable to create OCI Compute Client")
return core.ComputeClient{}, err
}

return computeClient, nil
}
101 changes: 101 additions & 0 deletions cloud/scope/clients_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package scope

import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"sync"

"github.com/oracle/cluster-api-provider-oci/cloud/config"
"github.com/oracle/cluster-api-provider-oci/cloud/services/compute"
"github.com/oracle/cluster-api-provider-oci/cloud/services/vcn"
"github.com/oracle/oci-go-sdk/v63/identity"
"github.com/oracle/oci-go-sdk/v63/networkloadbalancer"
"k8s.io/klog/v2/klogr"
)

type MockOCIClients struct {
VCNClient vcn.Client
LoadBalancerClient networkloadbalancer.NetworkLoadBalancerClient
IdentityClient identity.IdentityClient
ComputeClient compute.ComputeClient
}

var (
MockTestRegion = "us-lexington-1"
)

func MockNewClientProvider(mockClients MockOCIClients) (ClientProvider, error) {

clientsInject := map[string]OCIClients{MockTestRegion: {
VCNClient: mockClients.VCNClient,
LoadBalancerClient: mockClients.LoadBalancerClient,
IdentityClient: mockClients.IdentityClient,
ComputeClient: mockClients.ComputeClient,
}}

authConfig, err := MockAuthConfig()
if err != nil {
return ClientProvider{}, err
}

ociAuthConfigProvider, err := config.NewConfigurationProvider(&authConfig)
if err != nil {
fmt.Printf("expected ociAuthConfigProvider to be created %s \n", err)
return ClientProvider{}, err
}
log := klogr.New()
clientProvider := ClientProvider{
Logger: &log,
ociClients: clientsInject,
ociClientsLock: new(sync.RWMutex),
ociAuthConfigProvider: ociAuthConfigProvider,
authConfig: &authConfig,
}

return clientProvider, nil
}

func MockAuthConfig() (config.AuthConfig, error) {
privateKey, err := generatePrivateKeyPEM()
if err != nil {
fmt.Println("error generating a private key")
return config.AuthConfig{}, err
}

authConfig := config.AuthConfig{
UseInstancePrincipals: false,
Region: MockTestRegion,
Fingerprint: "mock-finger-print",
PrivateKey: privateKey,
UserID: "ocid1.tenancy.oc1..<unique_ID>",
TenancyID: "ocid1.tenancy.oc1..<unique_ID>",
}

return authConfig, nil
}

func generatePrivateKeyPEM() (string, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
return "", err
}

privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
privateKeyBlock := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: privateKeyBytes,
}

var privateKeyBuf bytes.Buffer
err = pem.Encode(&privateKeyBuf, privateKeyBlock)
if err != nil {
fmt.Printf("error when encode private pem: %s \n", err)
return "", err
}

return privateKeyBuf.String(), err
}
86 changes: 86 additions & 0 deletions cloud/scope/clients_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package scope

import (
"reflect"
"testing"

"github.com/golang/mock/gomock"
"github.com/oracle/cluster-api-provider-oci/cloud/services/vcn/mock_vcn"
)

func TestClients_NewClientProvider(t *testing.T) {

authConfig, err := MockAuthConfig()
if err != nil {
t.Error(err)
}

clientProvider, err := NewClientProvider(MockTestRegion, &authConfig)
if err != nil {
t.Errorf("Expected %v to equal nil", err)
}

if reflect.DeepEqual(clientProvider, ClientProvider{}) {
t.Errorf("clientProvider can not be an empty struct")
}
}

func TestClients_BuildNewClients(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
vcnClient := mock_vcn.NewMockClient(mockCtrl)

clientProvider, err := MockNewClientProvider(MockOCIClients{
VCNClient: vcnClient,
})
if err != nil {
t.Errorf("Expected %v to equal nil", err)
}

clients, err := clientProvider.GetOrBuildClient(MockTestRegion)
if err != nil {
t.Errorf("Expected %v to equal nil", err)
}
vcn := clients.VCNClient

if vcn != vcnClient {
t.Errorf("Expected %v to equal %v", vcnClient, vcn)
}

// build clients for a region not in our provider list yet
clients, err = clientProvider.GetOrBuildClient("us-austin-1")
if err != nil {
t.Errorf("Expected %v to equal nil", err)
}
vcn = clients.VCNClient
if vcn == vcnClient {
t.Errorf("Expected %v to NOT equal %v", vcnClient, vcn)
}
}

func TestClients_ReuseClients(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
vcnClient := mock_vcn.NewMockClient(mockCtrl)

clientProvider, err := MockNewClientProvider(MockOCIClients{
VCNClient: vcnClient,
})
if err != nil {
t.Errorf("Expected %v to equal nil", err)
}

firstClients, err := clientProvider.GetOrBuildClient(MockTestRegion)
if err != nil {
t.Errorf("Expected %v to equal nil", err)
}

secondClients, err := clientProvider.GetOrBuildClient(MockTestRegion)
if err != nil {
t.Errorf("Expected %v to equal nil", err)
}

if &secondClients.VCNClient == &firstClients.VCNClient {
t.Errorf("Expected %v to equal %v", secondClients.VCNClient, firstClients.VCNClient)
}
}
Loading

0 comments on commit 70e961b

Please sign in to comment.