Skip to content

Commit

Permalink
Service Manager client (#674)
Browse files Browse the repository at this point in the history
* Add SM client

* Get secret for SM http client

* Build HTTP client for Service Manager

* Add method to fetch service offerings

* Add method to fetch service offerings

* Add Common struct with fields common to all SM objects

* Add ServiceOffering struct

* Add method to extract ServiceOffering metadata fields values

* Adjustments for running in main

* nil checks and log improvements

* Add method for setting client for given secret

* Fix not found error return

* Fix tests after adding ServiceInstance CRD existence check

* go mod tidy and goimports

* Handle k8s not found error when default secret does not exist
  • Loading branch information
szwedm authored and kyma-gopher-bot committed Aug 22, 2024
1 parent ec4ddcf commit f531026
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 66 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/prometheus/client_golang v1.20.1
github.com/stretchr/testify v1.9.0
go.uber.org/zap v1.27.0
golang.org/x/oauth2 v0.22.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.31.0
k8s.io/apiextensions-apiserver v0.31.0
Expand Down Expand Up @@ -57,7 +58,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
Expand Down
21 changes: 19 additions & 2 deletions internal/cluster-object/secret_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ func (p *SecretProvider) All(ctx context.Context) (*corev1.SecretList, error) {
return nil, err
}

if err := p.getSecretsFromRefInServiceInstances(ctx, siList, secrets); err != nil {
return nil, err
if siList != nil && len(siList.Items) > 0 {
if err := p.getSecretsFromRefInServiceInstances(ctx, siList, secrets); err != nil {
return nil, err
}
}

if len(secrets.Items) == 0 {
Expand Down Expand Up @@ -141,3 +143,18 @@ func (p *SecretProvider) secretExistsInList(secret *corev1.Secret, secrets *core
}
return false
}

func (p *SecretProvider) GetByNameAndNamespace(ctx context.Context, name, namespace string) (*corev1.Secret, error) {
p.logger.Info(fmt.Sprintf("fetching \"%s\" secret in \"%s\" namespace", name, namespace))
secret := &corev1.Secret{}
if err := p.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, secret); err != nil {
if k8serrors.IsNotFound(err) {
p.logger.Warn(fmt.Sprintf("secret \"%s\" not found in \"%s\" namespace", name, namespace))
return nil, err
}
p.logger.Error(fmt.Sprintf("failed to fetch \"%s\" secret in \"%s\" namespace", name, namespace), "error", err)
return nil, err
}

return secret, nil
}
20 changes: 20 additions & 0 deletions internal/cluster-object/secret_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,24 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestSecretProvider(t *testing.T) {
// given
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
scheme := clientgoscheme.Scheme
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
crd := &apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: siCrdName,
},
}

t.Run("should fetch all secrets - from the module's namespace, with a namespace prefix, with an arbitrary name", func(t *testing.T) {
// given
Expand Down Expand Up @@ -76,6 +86,8 @@ func TestSecretProvider(t *testing.T) {
ns.Items = append(ns.Items, additionalNamespaces...)

k8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
WithLists(ns, sis, secrets).
WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer).
Build()
Expand Down Expand Up @@ -113,6 +125,8 @@ func TestSecretProvider(t *testing.T) {
ns.Items = append(ns.Items, additionalNamespaces...)

k8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
WithLists(ns, sis, secrets).
WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer).
Build()
Expand Down Expand Up @@ -150,6 +164,8 @@ func TestSecretProvider(t *testing.T) {
ns.Items = append(ns.Items, additionalNamespaces...)

k8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
WithLists(ns, sis, secrets).
WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer).
Build()
Expand Down Expand Up @@ -196,6 +212,8 @@ func TestSecretProvider(t *testing.T) {
ns.Items = append(ns.Items, additionalNamespaces...)

k8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
WithLists(ns, sis, secrets).
WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer).
Build()
Expand All @@ -221,6 +239,8 @@ func TestSecretProvider(t *testing.T) {
ns.Items = append(ns.Items, additionalNamespaces...)

k8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
WithLists(ns, sis, secrets).
WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer).
Build()
Expand Down
36 changes: 36 additions & 0 deletions internal/cluster-object/service_instance_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ import (
"context"
"fmt"
"log/slog"
"strings"

"github.com/kyma-project/btp-manager/controllers"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
serviceInstanceProviderName = "ServiceInstanceProvider"
secretRefKey = "btpAccessCredentialsSecret"
siCrdName = "serviceinstances.services.cloud.sap.com"
)

type ServiceInstanceProvider struct {
Expand All @@ -36,6 +41,10 @@ func (p *ServiceInstanceProvider) AllWithSecretRef(ctx context.Context) (*unstru
return nil, err
}

if filtered == nil || len(filtered.Items) == 0 {
return nil, nil
}

if err := p.filterBySecretRef(filtered); err != nil {
p.logger.Error("while filtering service instances by secret ref", "error", err)
return nil, err
Expand All @@ -45,6 +54,16 @@ func (p *ServiceInstanceProvider) AllWithSecretRef(ctx context.Context) (*unstru
}

func (p *ServiceInstanceProvider) All(ctx context.Context) (*unstructured.UnstructuredList, error) {
siCrdExists, err := p.crdExists(ctx, controllers.InstanceGvk)
if err != nil {
p.logger.Error("failed to check if ServiceInstance CRD exists", "error", err)
return nil, err
}
if !siCrdExists {
p.logger.Info("cannot fetch SAP BTP service operator secrets from ServiceInstances due to missing CRD")
return nil, nil
}

list := &unstructured.UnstructuredList{}
list.SetGroupVersionKind(controllers.InstanceGvk)
if err := p.List(ctx, list); err != nil {
Expand Down Expand Up @@ -84,3 +103,20 @@ func (p *ServiceInstanceProvider) hasSecretRef(item unstructured.Unstructured) (

return found, nil
}

func (p *ServiceInstanceProvider) crdExists(ctx context.Context, gvk schema.GroupVersionKind) (bool, error) {
crdName := fmt.Sprintf("%ss.%s", strings.ToLower(gvk.Kind), gvk.Group)
crd := &apiextensionsv1.CustomResourceDefinition{}

if err := p.Get(ctx, client.ObjectKey{Name: crdName}, crd); err != nil {
if k8serrors.IsNotFound(err) {
p.logger.Info(fmt.Sprintf("%s CRD does not exist", crdName))
return false, nil
} else {
p.logger.Error("failed to get CRD", "name", crdName, "error", err)
return false, err
}
}

return true, nil
}
23 changes: 21 additions & 2 deletions internal/cluster-object/service_instance_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,33 @@ import (
"github.com/kyma-project/btp-manager/controllers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestServiceInstanceProvider(t *testing.T) {
// given
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
scheme := clientgoscheme.Scheme
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
crd := &apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: siCrdName,
},
}

t.Run("should fetch all service instances", func(t *testing.T) {
// given
givenSiList := initServiceInstances(t)
k8sClient := fake.NewClientBuilder().WithLists(givenSiList).Build()
k8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
WithLists(givenSiList).
Build()
siProvider := NewServiceInstanceProvider(k8sClient, logger)

// when
Expand All @@ -34,7 +49,11 @@ func TestServiceInstanceProvider(t *testing.T) {
t.Run("should fetch service instances with secret reference", func(t *testing.T) {
// given
givenSiList := initServiceInstances(t)
k8sClient := fake.NewClientBuilder().WithLists(givenSiList).Build()
k8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(crd).
WithLists(givenSiList).
Build()
siProvider := NewServiceInstanceProvider(k8sClient, logger)

// when
Expand Down
158 changes: 158 additions & 0 deletions internal/service-manager/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package servicemanager

import (
"context"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"time"

clusterobject "github.com/kyma-project/btp-manager/internal/cluster-object"
"github.com/kyma-project/btp-manager/internal/service-manager/types"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/json"
)

const (
componentName = "ServiceManagerClient"
defaultSecret = "sap-btp-service-operator"
defaultNamespace = "kyma-system"
ServiceOfferingsPath = "/v1/service_offerings"
)

type Config struct {
ClientID string
ClientSecret string
URL string
TokenURL string
TokenURLSuffix string
}

type Client struct {
ctx context.Context
logger *slog.Logger
secretProvider clusterobject.NamespacedProvider[*corev1.Secret]
httpClient *http.Client
smURL string
}

func NewClient(ctx context.Context, logger *slog.Logger, secretProvider clusterobject.NamespacedProvider[*corev1.Secret]) *Client {
return &Client{
ctx: ctx,
logger: logger.With("component", componentName),
secretProvider: secretProvider,
}
}

func (c *Client) Defaults(ctx context.Context) error {
if err := c.buildHTTPClient(ctx, defaultSecret, defaultNamespace); err != nil {
if k8serrors.IsNotFound(err) {
c.logger.Warn(fmt.Sprintf("%s secret not found in %s namespace", defaultSecret, defaultNamespace))
return nil
}
c.logger.Error("failed to build http client", "error", err)
return err
}

return nil
}

func (c *Client) SetForGivenSecret(ctx context.Context, secretName, secretNamespace string) error {
if err := c.buildHTTPClient(ctx, secretName, secretNamespace); err != nil {
c.logger.Error("failed to build http client", "error", err)
return err
}

return nil
}

func (c *Client) buildHTTPClient(ctx context.Context, secretName, secretNamespace string) error {
cfg, err := c.getSMConfigFromGivenSecret(ctx, secretName, secretNamespace)
if err != nil {
return err
}

oauth2ClientCfg := &clientcredentials.Config{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
TokenURL: cfg.TokenURL + cfg.TokenURLSuffix,
}
httpClient := preconfiguredHTTPClient()
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)

c.smURL = cfg.URL
c.httpClient = oauth2.NewClient(ctx, oauth2ClientCfg.TokenSource(ctx))

return nil
}

func (c *Client) getSMConfigFromGivenSecret(ctx context.Context, secretName, secretNamespace string) (*Config, error) {
secret, err := c.secretProvider.GetByNameAndNamespace(ctx, secretName, secretNamespace)
if err != nil {
if k8serrors.IsNotFound(err) {
c.logger.Warn("secret not found", "name", secretName, "namespace", secretNamespace)
}
return nil, err
}

return &Config{
ClientID: string(secret.Data["clientid"]),
ClientSecret: string(secret.Data["clientsecret"]),
URL: string(secret.Data["sm_url"]),
TokenURL: string(secret.Data["tokenurl"]),
TokenURLSuffix: string(secret.Data["tokenurlsuffix"]),
}, nil
}

func preconfiguredHTTPClient() *http.Client {
client := &http.Client{
Timeout: time.Second * 10,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
return client
}

func (c *Client) SetHTTPClient(httpClient *http.Client) {
c.httpClient = httpClient
}

func (c *Client) ServiceOfferings() (*types.ServiceOfferings, error) {
req, err := http.NewRequest(http.MethodGet, c.smURL+ServiceOfferingsPath, nil)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}

defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var serviceOfferings types.ServiceOfferings
if err := json.Unmarshal(body, &serviceOfferings); err != nil {
return nil, err
}

return &serviceOfferings, nil
}
Loading

0 comments on commit f531026

Please sign in to comment.