Skip to content

Commit

Permalink
[Misc] Server: SaaS callback payload enhanced (#83)
Browse files Browse the repository at this point in the history
When the CAPApplication is annotated with
"sme.sap.com/saas-additional-output" containing a valid JSON payload,
the corresponding JSON is now sent as additonalData along with other
payload duing async callback for provisioning.
  • Loading branch information
Pavan-SAP authored May 21, 2024
1 parent 9743265 commit a6d170d
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 19 deletions.
39 changes: 29 additions & 10 deletions cmd/server/internal/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import (
"github.com/sap/cap-operator/pkg/client/clientset/versioned"
)

const AnnotationSubscriptionContextSecret = "sme.sap.com/subscription-context-secret"
const (
AnnotationSubscriptionContextSecret = "sme.sap.com/subscription-context-secret"
AnnotationSaaSAdditionalOutput = "sme.sap.com/saas-additional-output"
)

const (
LabelBTPApplicationIdentifierHash = "sme.sap.com/btp-app-identifier-hash"
Expand Down Expand Up @@ -74,9 +77,10 @@ type SubscriptionHandler struct {
}

type CallbackResponse struct {
Status string `json:"status"`
Message string `json:"message"`
SubscriptionUrl string `json:"subscriptionUrl"`
Status string `json:"status"`
Message string `json:"message"`
SubscriptionUrl string `json:"subscriptionUrl"`
AdditionalOutput *map[string]any `json:"additionalOutput,omitempty"`
}
type OAuthResponse struct {
AccessToken string `json:"access_token"`
Expand Down Expand Up @@ -325,7 +329,21 @@ func (s *SubscriptionHandler) initializeCallback(tenantName string, ca *v1alpha1
status := s.checkCAPTenantStatus(ctx, ca.Namespace, tenantName, isProvisioning, saasData.CallbackTimeoutMillis)
klog.InfoS("CAPTenant check complete", "status", status)

s.handleAsyncCallback(ctx, saasData, status, asyncCallbackPath, appUrl, isProvisioning)
additionalOutput := &map[string]any{}
if isProvisioning {
saasAdditionalOutput := ca.Annotations[AnnotationSaaSAdditionalOutput]
if saasAdditionalOutput != "" {
// Add additional output to the callback response
err := json.Unmarshal([]byte(saasAdditionalOutput), additionalOutput)
if err != nil {
klog.ErrorS(err, "Error parsing additional output", "annotation value", saasAdditionalOutput)
additionalOutput = nil
}
}
} else {
additionalOutput = nil
}
s.handleAsyncCallback(ctx, saasData, status, asyncCallbackPath, appUrl, additionalOutput, isProvisioning)
}()

klog.InfoS("Waiting for async saas callback after checks...")
Expand Down Expand Up @@ -480,7 +498,7 @@ func prepareTokenRequest(ctx context.Context, saasData *util.SaasRegistryCredent
return tokenReq, nil
}

func (s *SubscriptionHandler) handleAsyncCallback(ctx context.Context, saasData *util.SaasRegistryCredentials, status bool, asyncCallbackPath string, appUrl string, isProvisioning bool) {
func (s *SubscriptionHandler) handleAsyncCallback(ctx context.Context, saasData *util.SaasRegistryCredentials, status bool, asyncCallbackPath string, appUrl string, additionalOutput *map[string]any, isProvisioning bool) {
// Get OAuth token
tokenClient := s.httpClientGenerator.NewHTTPClient()
tokenReq, err := prepareTokenRequest(ctx, saasData, tokenClient)
Expand Down Expand Up @@ -514,9 +532,10 @@ func (s *SubscriptionHandler) handleAsyncCallback(ctx context.Context, saasData
}

payload, _ := json.Marshal(&CallbackResponse{
Status: checkMatch(status, CallbackSucceeded, CallbackFailed),
Message: checkMatch(status, checkMatch(isProvisioning, ProvisioningSucceededMessage, DeprovisioningSucceededMessage), checkMatch(isProvisioning, ProvisioningFailedMessage, DeprovisioningFailedMessage)),
SubscriptionUrl: appUrl,
Status: checkMatch(status, CallbackSucceeded, CallbackFailed),
Message: checkMatch(status, checkMatch(isProvisioning, ProvisioningSucceededMessage, DeprovisioningSucceededMessage), checkMatch(isProvisioning, ProvisioningFailedMessage, DeprovisioningFailedMessage)),
SubscriptionUrl: appUrl,
AdditionalOutput: additionalOutput,
})
callbackReq, _ := http.NewRequestWithContext(ctx, http.MethodPut, saasData.SaasManagerUrl+asyncCallbackPath, bytes.NewBuffer(payload))
callbackReq.Header.Set("Content-Type", "application/json")
Expand All @@ -530,7 +549,7 @@ func (s *SubscriptionHandler) handleAsyncCallback(ctx context.Context, saasData
klog.ErrorS(err, "Error sending async callback")
return
} else {
klog.InfoS("Async callback done", "response", callbackResponse)
klog.InfoS("Async callback done", "response", callbackResponse.Body, "status", callbackResponse.Status)
defer callbackResponse.Body.Close()
}
}
Expand Down
60 changes: 51 additions & 9 deletions cmd/server/internal/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,14 +243,16 @@ func Test_IncorrectMethod(t *testing.T) {

func Test_provisioning(t *testing.T) {
tests := []struct {
name string
method string
body string
createCROs bool
withSecretKey bool
existingTenant bool
expectedStatusCode int
expectedResponse Result
name string
method string
body string
createCROs bool
withAdditionalData bool
invalidAdditionalData bool
withSecretKey bool
existingTenant bool
expectedStatusCode int
expectedResponse Result
}{
{
name: "Invalid Provisioning Request",
Expand Down Expand Up @@ -290,6 +292,31 @@ func Test_provisioning(t *testing.T) {
Message: ResourceCreated,
},
},
{
name: "Provisioning Request valid with additional data and existing tenant",
method: http.MethodPut,
body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`,
createCROs: true,
withAdditionalData: true,
existingTenant: true,
expectedStatusCode: http.StatusAccepted,
expectedResponse: Result{
Message: ResourceCreated,
},
},
{
name: "Provisioning Request valid with invalid additional data and existing tenant",
method: http.MethodPut,
body: `{"subscriptionAppName":"` + appName + `","globalAccountGUID":"` + globalAccountId + `","subscribedTenantId":"` + tenantId + `","subscribedSubdomain":"` + subDomain + `"}`,
createCROs: true,
withAdditionalData: true,
invalidAdditionalData: true,
existingTenant: true,
expectedStatusCode: http.StatusAccepted,
expectedResponse: Result{
Message: ResourceCreated,
},
},
{
name: "Provisioning Request with existing tenant",
method: http.MethodPut,
Expand All @@ -309,9 +336,16 @@ func Test_provisioning(t *testing.T) {
var cat *v1alpha1.CAPTenant
if testData.createCROs {
ca = createCA()
if testData.withAdditionalData {
if !testData.invalidAdditionalData {
ca.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{\"foo\":\"bar\"}"}
} else {
ca.Annotations = map[string]string{AnnotationSaaSAdditionalOutput: "{foo\":\"bar\"}"} //invalid json
}
}
}
if testData.existingTenant {
cat = createCAT(false)
cat = createCAT(testData.withAdditionalData)
}
client, tokenString, err := SetupValidTokenAndIssuerForSubscriptionTests("appname!b14")
if err != nil {
Expand Down Expand Up @@ -454,6 +488,7 @@ func TestAsyncCallback(t *testing.T) {
testName string
status bool
useCredentialType string
additionalData *map[string]any
isProvisioning bool
}
saasData := &util.SaasRegistryCredentials{
Expand Down Expand Up @@ -512,6 +547,7 @@ func TestAsyncCallback(t *testing.T) {
if err != nil {
t.Fatalf("could not read callback request body: %s", err.Error())
}
t.Logf("Async callback payload = %s", body)
err = json.Unmarshal(body, payload)
if err != nil {
t.Fatalf("could not parse callback request body: %s", err.Error())
Expand All @@ -528,6 +564,9 @@ func TestAsyncCallback(t *testing.T) {
t.Fatal("incorrect message in payload")
}
}
if params.additionalData != nil && payload.AdditionalOutput == nil {
t.Fatal("expected additional output in payload")
}
w.WriteHeader(200)
}
}))
Expand Down Expand Up @@ -566,6 +605,8 @@ func TestAsyncCallback(t *testing.T) {
{testName: "1", status: true, useCredentialType: "x509", isProvisioning: true},
{testName: "2", status: true, useCredentialType: "x509", isProvisioning: false},
{testName: "3", status: false, useCredentialType: "instance-secret", isProvisioning: true},
{testName: "4", status: false, useCredentialType: "instance-secret", additionalData: &map[string]any{"foo": "bar"}, isProvisioning: true},
{testName: "5", status: false, useCredentialType: "x509", additionalData: &map[string]any{"foo1": "bar2", "someKey": &map[string]string{"name": "key", "plan": "none"}}, isProvisioning: true},
}

ctx := context.WithValue(context.Background(), cKey, true)
Expand All @@ -580,6 +621,7 @@ func TestAsyncCallback(t *testing.T) {
p.status,
"/async/callback",
"https://app.cluster.local",
p.additionalData,
p.isProvisioning,
)
})
Expand Down

0 comments on commit a6d170d

Please sign in to comment.