From a6d170d305c0ea7733b7643468104bc5c647b4cc Mon Sep 17 00:00:00 2001 From: Pavan <25031267+Pavan-SAP@users.noreply.github.com> Date: Tue, 21 May 2024 15:46:50 +0200 Subject: [PATCH] [Misc] Server: SaaS callback payload enhanced (#83) 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. --- cmd/server/internal/handler.go | 39 ++++++++++++++----- cmd/server/internal/handler_test.go | 60 ++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/cmd/server/internal/handler.go b/cmd/server/internal/handler.go index be158ec..68ab894 100644 --- a/cmd/server/internal/handler.go +++ b/cmd/server/internal/handler.go @@ -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" @@ -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"` @@ -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...") @@ -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) @@ -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") @@ -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() } } diff --git a/cmd/server/internal/handler_test.go b/cmd/server/internal/handler_test.go index 5adc815..018fc8a 100644 --- a/cmd/server/internal/handler_test.go +++ b/cmd/server/internal/handler_test.go @@ -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", @@ -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, @@ -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 { @@ -454,6 +488,7 @@ func TestAsyncCallback(t *testing.T) { testName string status bool useCredentialType string + additionalData *map[string]any isProvisioning bool } saasData := &util.SaasRegistryCredentials{ @@ -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()) @@ -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) } })) @@ -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) @@ -580,6 +621,7 @@ func TestAsyncCallback(t *testing.T) { p.status, "/async/callback", "https://app.cluster.local", + p.additionalData, p.isProvisioning, ) })