Skip to content

Commit

Permalink
Secret availability in Service Binding (#802)
Browse files Browse the repository at this point in the history
* Add cluster id label to secret if it has value in SB

* Add Secret's name and namespace in the response for SB

* Get secret for given service binding ID from the cluster

* Add unit tests for GET Service Binding(s) with available secret

* Revert timeouts in tests

* Fix service instance ID in unit test

* Remove unused objs

* Adjust NumItems field for returned ServiceBindings in test
  • Loading branch information
szwedm committed Aug 21, 2024
1 parent 96a9bdd commit f378e27
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 26 deletions.
47 changes: 40 additions & 7 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ func (a *API) ListServiceBindings(writer http.ResponseWriter, request *http.Requ
a.handleError(writer, err)
return
}
sbsVM, err := responses.ToServiceBindingsVM(sbs)
sbSecrets := a.ServiceBindingsSecrets(sbs)
sbsVM, err := responses.ToServiceBindingsVM(sbs, sbSecrets)
if err != nil {
a.handleError(writer, err)
return
Expand Down Expand Up @@ -271,6 +272,15 @@ func (a *API) GetServiceBinding(writer http.ResponseWriter, request *http.Reques
a.handleError(writer, err)
return
}
secrets, err := a.secretsForGivenServiceBindingID(sb.ID)
if err != nil {
a.handleError(writer, err)
return
}
if len(secrets.Items) > 0 {
sbVM.SecretName = secrets.Items[0].Name
sbVM.SecretNamespace = secrets.Items[0].Namespace
}
response, err := json.Marshal(sbVM)
if err != nil {
a.handleError(writer, err)
Expand All @@ -282,11 +292,7 @@ func (a *API) GetServiceBinding(writer http.ResponseWriter, request *http.Reques
func (a *API) DeleteServiceBinding(writer http.ResponseWriter, request *http.Request) {
a.setupCors(writer, request)
id := request.PathValue("id")
filterLabels := map[string]string{
clusterobject.ManagedByLabelKey: clusterobject.OperatorName,
clusterobject.ServiceBindingIDLabel: id,
}
secrets, err := a.secretManager.GetAllByLabels(context.Background(), filterLabels)
secrets, err := a.secretsForGivenServiceBindingID(id)
if err != nil {
a.handleError(writer, err)
return
Expand All @@ -301,6 +307,15 @@ func (a *API) DeleteServiceBinding(writer http.ResponseWriter, request *http.Req
}
}

func (a *API) secretsForGivenServiceBindingID(sbID string) (*corev1.SecretList, error) {
filterLabels := map[string]string{
clusterobject.ManagedByLabelKey: clusterobject.OperatorName,
clusterobject.ServiceBindingIDLabel: sbID,
}
secrets, err := a.secretManager.GetAllByLabels(context.Background(), filterLabels)
return secrets, err
}

func (a *API) setupCors(writer http.ResponseWriter, request *http.Request) {
a.logger.Info(fmt.Sprintf("api call to -> %s as: %s", request.RequestURI, request.Method))
origin := request.Header.Get("Origin")
Expand Down Expand Up @@ -402,6 +417,22 @@ func (a *API) handleError(writer http.ResponseWriter, errToHandle error, fallbac
return
}

func (a *API) ServiceBindingsSecrets(sbs *types.ServiceBindings) responses.ServiceBindingSecret {
serviceBindingsSecrets := make(responses.ServiceBindingSecret, 0)
for _, sb := range sbs.Items {
secrets, err := a.secretsForGivenServiceBindingID(sb.ID)
if err != nil {
a.logger.Error("failed to get secrets for service binding", "service binding id", sb.ID, "error", err)
continue
}
if len(secrets.Items) > 0 {
serviceBindingsSecrets[sb.ID] = &secrets.Items[0]
}
}

return serviceBindingsSecrets
}

func generateSecretFromSISBData(si *types.ServiceInstance, sb *types.ServiceBinding, createSBRequest *requests.CreateServiceBinding) (*corev1.Secret, error) {
var secretName, secretNamespace string
var err error
Expand All @@ -426,7 +457,9 @@ func generateSecretFromSISBData(si *types.ServiceInstance, sb *types.ServiceBind
clusterobject.ServiceInstanceIDLabel: si.ID,
clusterobject.ServiceInstanceNameLabel: si.Name,
}

if sb.Labels != nil && sb.Labels[types.ClusterIDLabel][0] != "" {
labels[clusterobject.ClusterIDLabel] = sb.Labels[types.ClusterIDLabel][0]
}
creds, err := normalizeCredentials(sb.Credentials)
if err != nil {
return nil, fmt.Errorf("failed to normalize credentials for secret's data: %w", err)
Expand Down
135 changes: 131 additions & 4 deletions internal/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,61 @@ func TestAPI(t *testing.T) {
assert.ElementsMatch(t, sbs.Items, defaultSBs.Items)
})

t.Run("GET Service Bindings with Secrets", func(t *testing.T) {
// given
sb1ID, sb2ID := "550e8400-e29b-41d4-a716-446655440003", "9e420bca-4cf2-4858-ade2-e5ef23cd756f"
ns1, ns2 := "default", "kyma-system"
expectedSBs := defaultServiceBindingsWithSecrets()
secret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: sb1ID + "-secret",
Namespace: ns1,
Labels: map[string]string{
clusterobject.ManagedByLabelKey: clusterobject.OperatorName,
clusterobject.ServiceBindingIDLabel: sb1ID,
},
},
StringData: map[string]string{"username": "user1", "password": "pass1"},
}
secret2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: sb2ID + "-secret",
Namespace: ns2,
Labels: map[string]string{
clusterobject.ManagedByLabelKey: clusterobject.OperatorName,
clusterobject.ServiceBindingIDLabel: sb2ID,
},
},
StringData: map[string]string{"username": "user2", "password": "pass2"},
}

err := secretMgr.Create(context.TODO(), secret1)
require.NoError(t, err)
err = secretMgr.Create(context.TODO(), secret2)
require.NoError(t, err)

// when
req, err := http.NewRequest(http.MethodGet, apiAddr+"/api/service-bindings", nil)
resp, err := apiClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()

var sbs responses.ServiceBindings
err = json.NewDecoder(resp.Body).Decode(&sbs)
require.NoError(t, err)

// then
assert.Equal(t, sbs.NumItems, 4)
assert.ElementsMatch(t, sbs.Items, expectedSBs.Items)

//cleanup
err = secretMgr.Delete(context.TODO(), secret1)
require.NoError(t, err)
err = secretMgr.Delete(context.TODO(), secret2)
require.NoError(t, err)
})

t.Run("GET Service Bindings 403 error", func(t *testing.T) {
// when
fakeSM.RespondWithErrors()
Expand All @@ -324,7 +379,7 @@ func TestAPI(t *testing.T) {
t.Run("GET Service Binding by ID", func(t *testing.T) {
// given
sbID := "318a16c3-7c80-485f-b55c-918629012c9a"
expectedSI := getServiceBindingByID(defaultSBs, sbID)
expectedSB := getServiceBindingByID(defaultSBs, sbID)

// when
req, err := http.NewRequest(http.MethodGet, apiAddr+"/api/service-bindings/"+sbID, nil)
Expand All @@ -338,7 +393,46 @@ func TestAPI(t *testing.T) {
require.NoError(t, err)

// then
assert.Equal(t, expectedSI, sb)
assert.Equal(t, expectedSB, sb)
})

t.Run("GET Service Binding by ID with Secret", func(t *testing.T) {
// given
sbID, ns := "550e8400-e29b-41d4-a716-446655440003", "default"
sbs := defaultServiceBindingsWithSecrets()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: sbID + "-secret",
Namespace: ns,
Labels: map[string]string{
clusterobject.ManagedByLabelKey: clusterobject.OperatorName,
clusterobject.ServiceBindingIDLabel: sbID,
},
},
StringData: map[string]string{"username": "user1", "password": "pass1"},
}
err := secretMgr.Create(context.TODO(), secret)
require.NoError(t, err)

expectedSB := getServiceBindingByID(sbs, sbID)

// when
req, err := http.NewRequest(http.MethodGet, apiAddr+"/api/service-bindings/"+sbID, nil)
resp, err := apiClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()

var sb responses.ServiceBinding
err = json.NewDecoder(resp.Body).Decode(&sb)
require.NoError(t, err)

// then
assert.Equal(t, expectedSB, sb)

// cleanup
err = secretMgr.Delete(context.TODO(), secret)
require.NoError(t, err)
})

t.Run("GET Service Binding by ID 400 error", func(t *testing.T) {
Expand Down Expand Up @@ -400,6 +494,7 @@ func TestAPI(t *testing.T) {

t.Run("POST Service Binding with JSON object in credentials", func(t *testing.T) {
// given
siID := "a7e240d6-e348-4fc0-a54c-7b7bfe9b9da6"
sbCreateRequest := requests.CreateServiceBinding{
Name: "sb-test-02",
ServiceInstanceID: servicemanager.FakeJSONObjectCredentialsServiceInstanceID,
Expand Down Expand Up @@ -428,7 +523,7 @@ func TestAPI(t *testing.T) {
// when
secrets, err := secretMgr.GetAllByLabels(context.TODO(), map[string]string{
clusterobject.ServiceBindingIDLabel: sb.ID,
clusterobject.ServiceInstanceIDLabel: sbCreateRequest.ServiceInstanceID,
clusterobject.ServiceInstanceIDLabel: siID,
})
require.NoError(t, err)

Expand Down Expand Up @@ -600,7 +695,7 @@ func defaultServiceBindings() responses.ServiceBindings {
{
ID: "550e8400-e29b-41d4-a716-446655440003",
Name: "service-binding",
Credentials: map[string]interface{}{"username": "user", "password": "pass"},
Credentials: map[string]interface{}{"username": "user1", "password": "pass1"},
},
{
ID: "9e420bca-4cf2-4858-ade2-e5ef23cd756f",
Expand All @@ -615,8 +710,40 @@ func defaultServiceBindings() responses.ServiceBindings {
{
ID: "8e97d56b-9fc1-43db-9d2e-e52f8ce91046",
Name: "service-binding-4",
Credentials: map[string]interface{}{"username": "user4", "password": "pass4"},
},
},
}
}

func defaultServiceBindingsWithSecrets() responses.ServiceBindings {
return responses.ServiceBindings{
NumItems: 4,
Items: []responses.ServiceBinding{
{
ID: "550e8400-e29b-41d4-a716-446655440003",
Name: "service-binding",
Credentials: map[string]interface{}{"username": "user1", "password": "pass1"},
SecretName: "550e8400-e29b-41d4-a716-446655440003-secret",
SecretNamespace: "default",
},
{
ID: "9e420bca-4cf2-4858-ade2-e5ef23cd756f",
Name: "service-binding-2",
Credentials: map[string]interface{}{"username": "user2", "password": "pass2"},
SecretName: "9e420bca-4cf2-4858-ade2-e5ef23cd756f-secret",
SecretNamespace: "kyma-system",
},
{
ID: "318a16c3-7c80-485f-b55c-918629012c9a",
Name: "service-binding-3",
Credentials: map[string]interface{}{"username": "user3", "password": "pass3"},
},
{
ID: "8e97d56b-9fc1-43db-9d2e-e52f8ce91046",
Name: "service-binding-4",
Credentials: map[string]interface{}{"username": "user4", "password": "pass4"},
},
},
}
}
Expand Down
16 changes: 11 additions & 5 deletions internal/api/responses/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
v1 "k8s.io/api/core/v1"
)

type ServiceBindingSecret map[string]*v1.Secret

func ToSecretVM(list v1.SecretList) Secrets {
secrets := Secrets{
Items: []Secret{},
Expand Down Expand Up @@ -106,18 +108,22 @@ func ToServiceInstanceVM(instance *types.ServiceInstance) ServiceInstance {
}
}

func ToServiceBindingsVM(bindings *types.ServiceBindings) (ServiceBindings, error) {
func ToServiceBindingsVM(serviceBindings *types.ServiceBindings, serviceBindingSecrets ServiceBindingSecret) (ServiceBindings, error) {
toReturn := ServiceBindings{
NumItems: len(bindings.Items),
NumItems: len(serviceBindings.Items),
Items: []ServiceBinding{},
}

for _, binding := range bindings.Items {
binding, err := ToServiceBindingVM(&binding)
for _, sb := range serviceBindings.Items {
sbResponse, err := ToServiceBindingVM(&sb)
if err != nil {
return ServiceBindings{}, err
}
toReturn.Items = append(toReturn.Items, binding)
if secret, exists := serviceBindingSecrets[sb.ID]; exists {
sbResponse.SecretName = secret.Name
sbResponse.SecretNamespace = secret.Namespace
}
toReturn.Items = append(toReturn.Items, sbResponse)
}
return toReturn, nil
}
Expand Down
8 changes: 5 additions & 3 deletions internal/api/responses/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ type ServiceOfferingPlan struct {
}

type ServiceBinding struct {
ID string `json:"id"`
Name string `json:"name"`
Credentials map[string]interface{} `json:"credentials"`
ID string `json:"id"`
Name string `json:"name"`
Credentials map[string]interface{} `json:"credentials"`
SecretName string `json:"secret_name"`
SecretNamespace string `json:"secret_namespace"`
}

type ServiceBindings struct {
Expand Down
9 changes: 7 additions & 2 deletions internal/cluster-object/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,22 @@ func (p *FakeSecretManager) Clean() {

func (p *FakeSecretManager) GetAllByLabels(ctx context.Context, labels map[string]string) (*corev1.SecretList, error) {
items := make([]corev1.Secret, 0)
mustMatchLen := len(labels)
for _, secret := range p.secrets {
if secret.Labels == nil {
continue
}
matchingLabelsNum := 0
secretLabels := secret.Labels
for key, value := range labels {
if secretLabels[key] != value {
if secretLabels[key] == value {
matchingLabelsNum++
continue
}
}
items = append(items, *secret)
if matchingLabelsNum == mustMatchLen {
items = append(items, *secret)
}
}
return &corev1.SecretList{
Items: items,
Expand Down
1 change: 1 addition & 0 deletions internal/cluster-object/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ const (
ServiceInstanceIDLabel = "service-instance-id"
ServiceInstanceNameLabel = "service-instance-name"
ServiceBindingIDLabel = "service-binding-id"
ClusterIDLabel = "cluster-id"
)
8 changes: 4 additions & 4 deletions internal/service-manager/testdata/service_bindings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"account": "account"
},
"credentials": {
"username": "user",
"password": "pass"
"username": "user1",
"password": "pass1"
},
"bind_resource": {
"app_guid": "550e8400-e29b-41d4-a716-446655440003",
Expand Down Expand Up @@ -91,8 +91,8 @@
"account": "account-4"
},
"credentials": {
"username": "user3",
"password": "pass3"
"username": "user4",
"password": "pass4"
},
"bind_resource": {
"app_guid": "8e97d56b-9fc1-43db-9d2e-e52f8ce91046",
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/SecretsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { Secrets } from "../shared/models";
import Ok from "../shared/validator";
import api from "../shared/api";
import { Button, DynamicPageTitle, Menu, MenuItem, MessageStrip, ObjectStatus } from "@ui5/webcomponents-react";
import { Button, DynamicPageTitle, Menu, ObjectStatus } from "@ui5/webcomponents-react";

function SecretsView({ onSecretChanged }: { onSecretChanged: (secret: string) => void }) {
const [secrets, setSecrets] = useState<Secrets>();
Expand Down

0 comments on commit f378e27

Please sign in to comment.