Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
quovadis
Product and Topic Expert
Product and Topic Expert
0 Kudos

Configuration as code (CaC) with destinations.

Destinations are very handy and powerful mechanism to facilitate access to target systems and devices.

When it comes to SAP BTP destinations, the idea is to manage both subaccount and instance level destinations (and/or their certificates) as shared configuration resources on a provider subaccount level.

That way, the destinations configurations can be stored as versioned assets in a source repository and need to be maintained only once per provider, thus, without incurring application runtime tie-in.

Last but not least, BTP destination service is used as a self-configuration tool.

Configuration as code with SAP BTP destination service

 

Table of Contents
  1. Configuration as code with SAP BTP destination service.
    1. create shared destination service instance and binding.
  2. Provision bootstrap destinations.
    1. retrieve destination service credentials from binding.
    2. describe bootstrap destination definitions.
    3. create bootstrap destinations on subaccount.
  3. Configure destination resources.
    1. dynamic_dest route with managed approuter.
    2. SAP Cloud SDK built-in destinations.
  4. Documentation.

PS.

Bootstrap destinations definitions.

Even, if there is no intrinsic BTP CLI command to assist in creation of destinations from service bindings, this can be achieved quite easily with a bit of jq gimmick by applying service binding credentials to a json payload template, for instance:

 

{
    "init_data": {
        "subaccount": {
            "destinations": [
                  {
                    "Description": "dest-httpbin",
                    "Type": "HTTP",
                    "clientId": "sb-clone12847c4c89544b4f9234b26ede429f62!b282590|destination-xsappname!b62",
                    "HTML5.DynamicDestination": "true",
                    "HTML5.Timeout": "60000",
                    "Authentication": "OAuth2ClientCredentials",
                    "Name": "dest-httpbin",
                    "tokenServiceURL": "https://<subdomain>.authentication.us10.hana.ondemand.com/oauth/token",
                    "ProxyType": "Internet",
                    "URL": "https://httpbin.org",
                    "tokenServiceURLType": "Dedicated",
                    "clientSecret": "<clientSecret>"
                  },
                  {
                    "Description": "SAP Destination Service APIs",
                    "Type": "HTTP",
                    "clientId": "sb-clone12847c4c89544b4f9234b26ede429f62!b282590|destination-xsappname!b62",
                    "HTML5.DynamicDestination": "true",
                    "HTML5.Timeout": "60000",
                    "Authentication": "OAuth2ClientCredentials",
                    "Name": "destination-service",
                    "tokenServiceURL": "https://<subdomain>.authentication.us10.hana.ondemand.com/oauth/token",
                    "ProxyType": "Internet",
                    "URL": "https://destination-configuration.cfapps.us10.hana.ondemand.com/destination-configuration/v1",
                    "tokenServiceURLType": "Dedicated",
                    "clientSecret": "<clientSecret>"
                  }
            ],
           "certificates": [
           ],

            "existing_certificates_policy": "update",
            "existing_destinations_policy": "update"           
       }
   }
}

 

Alternatively, one could resort to using SAP Cloud SDK built-in service binding destinations.

The below nodejs code snippet demonstrates how to leverage SAP Cloud SDK with its service binding destinations with the likes of service manager and destinations services.

apiVersion: serverless.kyma-project.io/v1alpha2
kind: Function
metadata:
  name: {{ .Values.services.srv.name }}
  labels:
    {{- include "app.labels" . | nindent 4 }}
    app: {{ .Values.services.srv.name }}
spec:
  runtime: {{ .Values.services.srv.runtime }}
#  runtimeImageOverride: {{ .Values.services.srv.runtimeImageOverride }}
  source:
    inline:
      dependencies: |
        {
          "name": "{{ .Values.services.srv.name }}",
          "version": "0.0.1",
          "dependencies": {
            "axios":"latest"
            ,"debug": "latest"
            ,"@sap/xsenv": "latest"
            ,"@sap-cloud-sdk/http-client": "latest"
            ,"@sap-cloud-sdk/connectivity": "latest"
            ,"@sap-cloud-sdk/resilience": "latest"
            ,"async-retry": "latest"
          }
        }
      source: |
        const debug = require('debug')('{{ .Values.services.srv.name }}:function');
        const NOT_FOUND = 'Not Found';
        const xsenv = require('@sap/xsenv');

        const services = xsenv.getServices({
          sm: { label: 'service-manager', name: 'saas-sm' }
          ,
          dest: { label: 'destination' }

        });
        console.log('saas-sm: ', services.sm);

        const readServices = xsenv.readServices();
        console.log('readServices: ', readServices);

        const httpClient = require('@sap-cloud-sdk/http-client');

        const cloudSdkConnectivity = require('@sap-cloud-sdk/connectivity');
        const { retrieveJwt, decodeJwt, Destination } = require('@sap-cloud-sdk/connectivity');
        const { setGlobalLogLevel, createLogger } = require('@sap-cloud-sdk/util');
        const { retry } = require ('@sap-cloud-sdk/resilience');
        const { resilience } = require ('@sap-cloud-sdk/resilience');
        const ResilienceOptions = {
          retry: 10,
          circuitBreaker: false,
          timeout: 300*1000 // 5 minutes in milliseconds
        };          

        const retryme = require('async-retry');

        setGlobalLogLevel('debug');
        const logger = createLogger('http-logs');

        module.exports = {
          main: async function (event, context) {
            const req = event.extensions.request;

            const message = `Hello World`
              + ` from the Kyma Function ${context['function-name']}`
              + ` running on ${context.runtime}!`
              + ` with the request headers ${JSON.stringify(req.headers,0,2)}`;
            console.log(message);
            
            if (typeof req.path !== undefined) {
              console.log('path: ', JSON.stringify(req.path,0,2))
            }
            if (typeof req.params !== undefined) {
              console.log('params: ', JSON.stringify(req.params,0,2))
            }
            if (typeof req.url !== undefined) {
              console.log('url: ', JSON.stringify(req.url,0,2))
            }
            if (typeof req.authInfo !== undefined) {
              console.log('authInfo: ', JSON.stringify(req.authInfo,0,2))
            }

            const { pathname } = new URL(req.url || '', `https://${req.headers.host}`)
            console.log('pathname: ', pathname)

            const url = require("url");
            var url_parts = url.parse(req.url);
            console.log(url_parts);
            console.log(url_parts.pathname);

            // returns an array with paths
            let path_array = req.url.match('^[^?]*')[0].split('/').slice(1);
            console.log(path_array)

            console.log(req.url.match('^[^?]*')[0])

            if (!path_array?.length) return 'Please use an API verb';  
            const actions = [ 
               { name: 'offerings', verb: 'service_offerings', dest:  'saas-sm', url: '/v1/' },
               { name: 'plans', verb: 'service_plans', dest:  'saas-sm', url: '/v1/'  },
               { name: 'instances', verb: 'service_instances', dest:  'saas-sm', url: '/v1/'  },
               { name: 'bindings', verb: 'service_bindings', dest:  'saas-sm', url: '/v1/'  },
               { name: 'instanceDestinations', verb: 'instanceDestinations', dest:  'faas-dest-x509', url: '/destination-configuration/v1/'  },
               { name: 'subaccountDestinations', verb: 'subaccountDestinations', dest:  'faas-dest-x509' , url: '/destination-configuration/v1/' }
            ];
            
            const action = actions.find( ({ name }) => name === path_array[1] )
            console.log('action found: ', action)

            if (path_array[0] == 'srv' &&  action !== undefined) {

              path_array = req.url.match('^[^?]*')[0].split('/').slice(2);

              console.log('path_array: ', path_array)

              const queryString = req.query;
              console.log('queryString: ', queryString)
              const urlParams = new URLSearchParams(queryString);

              const params = req.params;
              console.log('params: ', params) 

              try {
                  // https://sap.github.io/cloud-sdk/docs/js/features/connectivity/destinations#service-binding-environment-variables
                  const endpoint =  path_array[1] !== undefined ? '/' + path_array[1] : '';
                  console.log(endpoint)
                  let res = await httpClient.executeHttpRequest({ destinationName: action.dest }, {
                      method: 'GET',
                      url: action.url + action.verb + endpoint
                  });
                  return res.data;
              } catch (err) {
                  console.log(err.stack);
                  return err.message;
              }
              
            }            
          }
        }
   scaleConfig:
    maxReplicas: 5
    minReplicas: 3
  resourceConfiguration:
    function:
      profile: S
  env: ## https://kyma-project.io/docs/kyma/latest/05-technical-reference/00-configuration-parameters/svls-02-environment-variables/#node-js-runtime-specific-environment-variables
    - name: FUNC_TIMEOUT ## Specifies the number of seconds in which a runtime must execute the code.
      value: '1800'
    - name: REQ_MB_LIMIT ## payload body size limit in megabytes.
      value: "10"

    - name: DEBUG
      value: '{{ .Values.services.srv.name }}:*'
    - name: SERVICE_BINDING_ROOT
      value: /bindings
    

  secretMounts: 
    - secretName: {{ .Values.services.sm.bindingSecretName }}
      mountPath: "/bindings/saas-sm"
    - secretName: {{ .Values.services.dest.bindingSecretNamex509 }}
      mountPath: "/bindings/faas-dest-x509"